From 962b47984527fe184f3f638c01521f43a6b3dc61 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sun, 16 Nov 2025 13:36:52 +0000 Subject: [PATCH 01/24] WIP --- dotnet/Directory.Packages.props | 3 +- dotnet/agent-framework-dotnet.slnx | 2 + dotnet/nuget.config | 1 + .../Agent_With_Anthropic.csproj | 19 + .../Agent_With_Anthropic/Program.cs | 20 ++ .../Agent_With_Anthropic/README.md | 16 + .../AnthropicChatClient.cs | 52 +++ .../AnthropicClientExtensions.cs | 52 +++ .../ChatClientHelper.cs | 338 ++++++++++++++++++ .../Microsoft.Agents.AI.Anthropic.csproj | 28 ++ 10 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs create mode 100644 dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 86d9e032cc..80ae322d07 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,6 +11,7 @@ + @@ -165,4 +166,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 908884476d..e36af91b11 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -44,6 +44,7 @@ + @@ -331,6 +332,7 @@ + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 76d943ce16..578b1054d2 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,6 +3,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..15fe5d99c5 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Agent_With_Anthropic.csproj @@ -0,0 +1,19 @@ + + + + Exe + net9.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..ae1bfdcb36 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend. + +using Anthropic.Client; +using Anthropic.Client.Core; +using Microsoft.Agents.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"; + +const string JokerInstructions = "You are good at telling jokes."; +const string JokerName = "JokerAgent"; + +var client = new AnthropicClient(new ClientOptions { APIKey = apiKey }); + +AIAgent agent = client.CreateAIAgent(model: model, instructions: JokerInstructions, name: JokerName); + +// Invoke the agent and output the text result. +// Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); 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..df0854ba2f --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md @@ -0,0 +1,16 @@ +# Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Azure Foundry service endpoint and deployment configured +- Azure CLI installed and authenticated (for Azure credential authentication) + +**Note**: This demo uses Azure CLI credentials for authentication. 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). + +Set the following environment variables: + +```powershell +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint +$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +``` diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs new file mode 100644 index 0000000000..dcc771a572 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable CA1812 + +using System.Text.Json; +using System.Text.Json.Serialization; +using Anthropic.Client; +using Anthropic.Client.Models.Beta.Messages; +using Anthropic.Client.Models.Messages; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Anthropic; + +/// +/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using +/// Azure-specific agent capabilities. +/// +internal sealed class AnthropicChatClient : IChatClient +{ + private readonly AnthropicClient _client; + private readonly ChatClientMetadata _metadata; + + internal AnthropicChatClient(AnthropicClient client, Uri? endpoint = null, string? defaultModelId = null) + { + this._client = client; + this._metadata = new ChatClientMetadata(providerName: "anthrendpointopic", providerUri: endpoint ?? new Uri("https://api.anthropic.com"), defaultModelId); + } + + public void Dispose() + { + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + Message messageResponse = await this._client.Messages.Create(ChatClientHelper.CreateMessageParameters(this, messages, options)); + throw new NotImplementedException(); + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + return (serviceKey is null && serviceType == typeof(AnthropicClient)) + ? this._client + : (serviceKey is null && serviceType == typeof(ChatClientMetadata)) + ? this._metadata + : null; + } + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} 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..9fd745a423 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Anthropic; +using Microsoft.Extensions.AI; + +namespace Anthropic.Client; + +/// +/// Provides extension methods for the class. +/// +public static class AnthropicClientExtensions +{ + /// + /// Creates a new AI agent using the specified model and options. + /// + /// The Anthropic client. + /// 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 created AI agent. + public static ChatClientAgent CreateAIAgent( + this AnthropicClient client, + string model, + string? instructions, + string? name = null, + string? description = null) + { + var options = new ChatClientAgentOptions + { + Instructions = instructions, + Name = name, + Description = description, + }; + + return new ChatClientAgent(client.AsIChatClient(model), options); + } + + /// + /// Get an compatible implementation around the . + /// + /// The Anthropic client. + /// The model to use for chat completions. + /// The implementation. + public static IChatClient AsIChatClient( + this AnthropicClient client, + string model) + { + return new AnthropicChatClient(client); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs new file mode 100644 index 0000000000..e008afbe55 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable CA1812 + +using System.Text.Json; +using System.Text.Json.Serialization; +using Anthropic.Client.Models.Messages; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Anthropic; + +/// +/// Helper class for chat client implementations +/// +internal static class ChatClientHelper +{ + /// + /// Create usage details from usage + /// + public static UsageDetails CreateUsageDetails(Usage usage) + { + UsageDetails usageDetails = new() + { + InputTokenCount = usage.InputTokens, + OutputTokenCount = usage.OutputTokens, + AdditionalCounts = [], + }; + + if (usage.CacheCreationInputTokens.HasValue) + { + usageDetails.AdditionalCounts.Add(nameof(usage.CacheCreationInputTokens), usage.CacheCreationInputTokens.Value); + } + + if (usage.CacheReadInputTokens.HasValue) + { + usageDetails.AdditionalCounts.Add(nameof(usage.CacheReadInputTokens), usage.CacheReadInputTokens.Value); + } + + return usageDetails; + } + + /// + /// Create message parameters from chat messages and options + /// + public static MessageCreateParams CreateMessageParameters(IChatClient client, IEnumerable messages, ChatOptions options) + { + if (options.RawRepresentationFactory?.Invoke(client) is not MessageCreateParams parameters) + { + parameters = new MessageCreateParams() + { + Model = options.ModelId!, + Messages = [], + System = (options.Instructions is string instructions) ? new SystemModel(instructions) : null, + MaxTokens = (options.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, + Temperature = (options.Temperature is float temperature) ? (double)temperature : null, + }; + } + + if (options is not null) + { + + if (options.TopP is float topP) + { + parameters.TopP = (decimal)topP; + } + + if (options.TopK is int topK) + { + parameters.TopK = topK; + } + + if (options.StopSequences is not null) + { + parameters.StopSequences = options.StopSequences.ToArray(); + } + + if (options.Tools is { Count: > 0 }) + { + parameters.ToolChoice ??= new(); + + if (options.ToolMode is RequiredChatToolMode r) + { + parameters.ToolChoice.Type = r.RequiredFunctionName is null ? ToolChoiceType.Any : ToolChoiceType.Tool; + parameters.ToolChoice.Name = r.RequiredFunctionName; + } + + IList tools = parameters.Tools ??= []; + foreach (var tool in options.Tools) + { + switch (tool) + { + case AIFunctionDeclaration f: + tools.Add(new Common.Tool(new Function(f.Name, f.Description, JsonSerializer.SerializeToNode(JsonSerializer.Deserialize(f.JsonSchema))))); + break; + + case HostedCodeInterpreterTool: + tools.Add(Common.Tool.CodeInterpreter); + break; + + case HostedWebSearchTool: + tools.Add(ServerTools.GetWebSearchTool(5)); + break; + +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + case HostedMcpServerTool mcpt: + MCPServer mcpServer = new() + { + Url = mcpt.ServerAddress, + Name = mcpt.ServerName, + }; + + if (mcpt.AllowedTools is not null) + { + mcpServer.ToolConfiguration.AllowedTools.AddRange(mcpt.AllowedTools); + } + + mcpServer.AuthorizationToken = mcpt.AuthorizationToken; + + (parameters.MCPServers ??= []).Add(mcpServer); + break; +#pragma warning restore MEAI001 + } + } + } + + // Map thinking parameters from ChatOptions + var thinkingParameters = options.GetThinkingParameters(); + if (thinkingParameters != null) + { + parameters.Thinking = thinkingParameters; + } + } + + foreach (ChatMessage message in messages) + { + if (message.Role == ChatRole.System) + { + (parameters.System ??= []).Add(new SystemMessage(string.Concat(message.Contents.OfType()))); + } + else + { + // Process contents in order, creating new messages when switching between tool results and other content + // This preserves ordering and handles interleaved tool calls, AI output, and tool results + Message currentMessage = null; + bool lastWasToolResult = false; + + foreach (AIContent content in message.Contents) + { + bool isToolResult = content is FunctionResultContent; + + // Create new message if: + // 1. This is the first content item, OR + // 2. We're switching between tool result and non-tool result content + if (currentMessage == null || lastWasToolResult != isToolResult) + { + currentMessage = new() + { + // Tool results must always be in User messages, others respect original role + Role = isToolResult ? RoleType.User : (message.Role == ChatRole.Assistant ? RoleType.Assistant : RoleType.User), + Content = [], + }; + parameters.Messages.Add(currentMessage); + lastWasToolResult = isToolResult; + } + + // Add content to current message + switch (content) + { + case FunctionResultContent frc: + currentMessage.Content.Add(new ToolResultContent() + { + ToolUseId = frc.CallId, + Content = new List() { new TextContent() { Text = frc.Result?.ToString() ?? string.Empty } }, + IsError = frc.Exception is not null, + }); + break; + + case TextReasoningContent reasoningContent: + if (string.IsNullOrEmpty(reasoningContent.Text)) + { + currentMessage.Content.Add(new Messaging.RedactedThinkingContent() { Data = reasoningContent.ProtectedData }); + } + else + { + currentMessage.Content.Add(new Messaging.ThinkingContent() + { + Thinking = reasoningContent.Text, + Signature = reasoningContent.ProtectedData, + }); + } + break; + + case TextContent textContent: + string text = textContent.Text; + if (currentMessage.Role == RoleType.Assistant) + { + text.TrimEnd(); + if (!string.IsNullOrWhiteSpace(text)) + { + currentMessage.Content.Add(new Anthropic.Client.Models.MessagesTextContent() { Text = text }); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + currentMessage.Content.Add(new TextContent() { Text = text }); + } + + break; + + case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): + currentMessage.Content.Add(new ContentBlock() + { + Source = new() + { + Data = Convert.ToBase64String(imageContent.Data.ToArray()), + MediaType = imageContent.MediaType, + } + }); + break; + + case DataContent documentContent when documentContent.HasTopLevelMediaType("application"): + currentMessage.Content.Add(new DocumentContent() + { + Source = new() + { + Data = Convert.ToBase64String(documentContent.Data.ToArray()), + MediaType = documentContent.MediaType, + } + }); + break; + + case FunctionCallContent fcc: + currentMessage.Content.Add(new ToolUseContent() + { + Id = fcc.CallId, + Name = fcc.Name, + Input = JsonSerializer.SerializeToNode(fcc.Arguments) + }); + break; + } + } + } + } + + parameters.Messages.RemoveAll(m => m.Content.Count == 0); + + // Avoid errors from completely empty input. + if (!parameters.Messages.Any(m => m.Content.Count > 0)) + { + parameters.Messages.Add(new(RoleType.User, "\u200b")); // zero-width space + } + + return parameters; + } + + /// + /// Process response content + /// + public static List ProcessResponseContent(MessageResponse response) + { + List contents = new(); + + foreach (ContentBase content in response.Content) + { + switch (content) + { + case Messaging.ThinkingContent thinkingContent: + contents.Add(new TextReasoningContent(thinkingContent.Thinking) + { + ProtectedData = thinkingContent.Signature, + }); + break; + + case Messaging.RedactedThinkingContent redactedThinkingContent: + contents.Add(new TextReasoningContent(null) + { + ProtectedData = redactedThinkingContent.Data, + }); + break; + + case TextContent tc: + var textContent = new TextContent(tc.Text); + if (tc.Citations != null && tc.Citations.Any()) + { + foreach (var tau in tc.Citations) + { + (textContent.Annotations ?? []).Add(new CitationAnnotation + { + RawRepresentation = tau, + AnnotatedRegions = + [ + new TextSpanAnnotatedRegion + { StartIndex = (int?)tau.StartPageNumber, EndIndex = (int?)tau.EndPageNumber } + ], + FileId = tau.Title + }); + } + } + contents.Add(textContent); + break; + + case ImageContent ic: + contents.Add(new DataContent(ic.Source.Data, ic.Source.MediaType)); + break; + + case ToolUseContent tuc: + contents.Add(new FunctionCallContent( + tuc.Id, + tuc.Name, + tuc.Input is not null ? tuc.Input.Deserialize>() : null)); + break; + + case ToolResultContent trc: + contents.Add(new FunctionResultContent( + trc.ToolUseId, + trc.Content)); + break; + } + } + + return contents; + } + + /// + /// Function parameters class + /// + private sealed class FunctionParameters + { + [JsonPropertyName("type")] + public string Type { get; set; } = "object"; + + [JsonPropertyName("required")] + public List Required { get; set; } = []; + + [JsonPropertyName("properties")] + public Dictionary Properties { get; set; } = []; + } +} 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..d8bf0b0a4b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -0,0 +1,28 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + preview + enable + true + + + + + + + + + + + + + + + + Microsoft Agent Framework for Foundry Agents + Provides Microsoft Agent Framework support for Foundry Agents. + + + From a9077ab92569c5ea7ac30a6a5939c19612924181 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:37:16 +0000 Subject: [PATCH 02/24] WIP --- .../ChatClientHelper.cs | 141 ++++++++++-------- 1 file changed, 79 insertions(+), 62 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs index e008afbe55..0b0c32f837 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs @@ -2,6 +2,7 @@ #pragma warning disable CA1812 +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Anthropic.Client.Models.Messages; @@ -39,6 +40,21 @@ public static UsageDetails CreateUsageDetails(Usage usage) return usageDetails; } + private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclaration function) + => new(function.JsonSchema.EnumerateObject().ToDictionary(k => k.Name, v => v.Value)); + + public static ThinkingConfigParam? GetThinkingParameters(this ChatOptions options) + { + const string ThinkingParametersKey = "Anthropic.ThinkingParameters"; + + if (options?.AdditionalProperties?.TryGetValue(ThinkingParametersKey, out var value) == true) + { + return value as ThinkingConfigParam; + } + + return null; + } + /// /// Create message parameters from chat messages and options /// @@ -46,62 +62,38 @@ public static MessageCreateParams CreateMessageParameters(IChatClient client, IE { if (options.RawRepresentationFactory?.Invoke(client) is not MessageCreateParams parameters) { - parameters = new MessageCreateParams() - { - Model = options.ModelId!, - Messages = [], - System = (options.Instructions is string instructions) ? new SystemModel(instructions) : null, - MaxTokens = (options.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, - Temperature = (options.Temperature is float temperature) ? (double)temperature : null, - }; - } - - if (options is not null) - { - - if (options.TopP is float topP) - { - parameters.TopP = (decimal)topP; - } - - if (options.TopK is int topK) - { - parameters.TopK = topK; - } - - if (options.StopSequences is not null) - { - parameters.StopSequences = options.StopSequences.ToArray(); - } + List? tools = null; + ToolChoice? toolChoice = null; if (options.Tools is { Count: > 0 }) { - parameters.ToolChoice ??= new(); - if (options.ToolMode is RequiredChatToolMode r) { - parameters.ToolChoice.Type = r.RequiredFunctionName is null ? ToolChoiceType.Any : ToolChoiceType.Tool; - parameters.ToolChoice.Name = r.RequiredFunctionName; + toolChoice = r.RequiredFunctionName is null ? new ToolChoice(new ToolChoiceAny()) : new ToolChoice(new ToolChoiceTool(r.RequiredFunctionName)); } - IList tools = parameters.Tools ??= []; + tools = []; foreach (var tool in options.Tools) { switch (tool) { case AIFunctionDeclaration f: - tools.Add(new Common.Tool(new Function(f.Name, f.Description, JsonSerializer.SerializeToNode(JsonSerializer.Deserialize(f.JsonSchema))))); - break; - - case HostedCodeInterpreterTool: - tools.Add(Common.Tool.CodeInterpreter); - break; - - case HostedWebSearchTool: - tools.Add(ServerTools.GetWebSearchTool(5)); + tools.Add(new ToolUnion(new Tool() + { + Name = f.Name, + Description = f.Description, + InputSchema = AIFunctionDeclarationToInputSchema(f) + })); break; -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + /* + case HostedCodeInterpreterTool: + tools.Add(new ToolUnion(CodeInterpreter)); + break; + case HostedWebSearchTool: + tools.Add(new ToolUnion(ServerTools.GetWebSearchTool(5))); + break; + #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. case HostedMcpServerTool mcpt: MCPServer mcpServer = new() { @@ -119,32 +111,65 @@ public static MessageCreateParams CreateMessageParameters(IChatClient client, IE (parameters.MCPServers ??= []).Add(mcpServer); break; #pragma warning restore MEAI001 + */ } } } - // Map thinking parameters from ChatOptions - var thinkingParameters = options.GetThinkingParameters(); - if (thinkingParameters != null) + parameters = new MessageCreateParams() { - parameters.Thinking = thinkingParameters; - } + Model = options.ModelId!, + Messages = GetMessages(messages), + System = GetSystem(options, messages), + MaxTokens = (options.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, + Temperature = (options.Temperature is float temperature) ? (double)temperature : null, + TopP = (options.TopP is float topP) ? (double)topP : null, + TopK = (options.TopK is int topK) ? topK : null, + StopSequences = (options.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : null, + ToolChoice = toolChoice, + Tools = tools, + Thinking = options.GetThinkingParameters(), + }; + } + + // Avoid errors from completely empty input. + if (!parameters.Messages.Any(m => m.Content.Count > 0)) + { + parameters.Messages.Add(new(RoleType.User, "\u200b")); // zero-width space } + return parameters; + } + + private static SystemModel? GetSystem(ChatOptions options, IEnumerable messages) + { + StringBuilder? fullInstructions = (options.Instructions is string instructions) ? new(instructions) : null; + foreach (ChatMessage message in messages) { if (message.Role == ChatRole.System) { - (parameters.System ??= []).Add(new SystemMessage(string.Concat(message.Contents.OfType()))); + (fullInstructions ??= new()).AppendLine(string.Concat(message.Contents.OfType())); } - else + } + + return fullInstructions is not null ? new SystemModel(fullInstructions.ToString()) : null; + } + + private static List GetMessages(IEnumerable chatMessages) + { + List messages = []; + + foreach (ChatMessage chatMessage in chatMessages) + { + if (chatMessage.Role != ChatRole.System) { // Process contents in order, creating new messages when switching between tool results and other content // This preserves ordering and handles interleaved tool calls, AI output, and tool results - Message currentMessage = null; + MessageParam? currentMessage = null; bool lastWasToolResult = false; - foreach (AIContent content in message.Contents) + foreach (AIContent content in chatMessage.Contents) { bool isToolResult = content is FunctionResultContent; @@ -156,10 +181,10 @@ public static MessageCreateParams CreateMessageParameters(IChatClient client, IE currentMessage = new() { // Tool results must always be in User messages, others respect original role - Role = isToolResult ? RoleType.User : (message.Role == ChatRole.Assistant ? RoleType.Assistant : RoleType.User), - Content = [], + Role = isToolResult ? RoleType.User : (chatMessage.Role == ChatRole.Assistant ? RoleType.Assistant : RoleType.User), + Content = new ContentModel(), }; - parameters.Messages.Add(currentMessage); + messages.Add(currentMessage); lastWasToolResult = isToolResult; } @@ -243,14 +268,6 @@ public static MessageCreateParams CreateMessageParameters(IChatClient client, IE } parameters.Messages.RemoveAll(m => m.Content.Count == 0); - - // Avoid errors from completely empty input. - if (!parameters.Messages.Any(m => m.Content.Count > 0)) - { - parameters.Messages.Add(new(RoleType.User, "\u200b")); // zero-width space - } - - return parameters; } /// From 179fcce38c175dd8635cf501888e53bd02b75b2f Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:32:02 +0000 Subject: [PATCH 03/24] Simple call working --- dotnet/agent-framework-dotnet.slnx | 3 + dotnet/nuget.config | 2 +- .../Agent_With_Anthropic/Program.cs | 5 +- .../AnthropicBetaChatClient.cs | 72 +++ .../AnthropicChatClient.cs | 52 -- .../AnthropicClientExtensions.cs | 2 +- .../ChatClientHelper.cs | 555 ++++++++++++------ .../Microsoft.Agents.AI.Anthropic.csproj | 5 +- 8 files changed, 444 insertions(+), 252 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index e36af91b11..445016fb51 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -386,4 +386,7 @@ + + + diff --git a/dotnet/nuget.config b/dotnet/nuget.config index 578b1054d2..a8522c3741 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,7 +3,7 @@ - + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index ae1bfdcb36..f6ce9d2b84 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -2,8 +2,9 @@ // This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend. +using Anthropic; using Anthropic.Client; -using Anthropic.Client.Core; +using Anthropic.Core; using Microsoft.Agents.AI; var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); @@ -17,4 +18,4 @@ AIAgent agent = client.CreateAIAgent(model: model, instructions: JokerInstructions, name: JokerName); // Invoke the agent and output the text result. -// Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); +Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs new file mode 100644 index 0000000000..a605b0b575 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable CA1812 + +using System.Text.Json; +using System.Text.Json.Serialization; +using Anthropic; +using Anthropic.Models.Beta.Messages; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Anthropic; + +/// +/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using +/// Azure-specific agent capabilities. +/// +internal sealed class AnthropicBetaChatClient : IChatClient +{ + private readonly AnthropicClient _client; + private readonly ChatClientMetadata _metadata; + + internal AnthropicBetaChatClient(AnthropicClient client, Uri? endpoint = null, string? defaultModelId = null) + { + this._client = client; + this._metadata = new ChatClientMetadata(providerName: "anthropic", providerUri: endpoint ?? new Uri("https://api.anthropic.com"), defaultModelId); + } + + public void Dispose() + { + } + + public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + var modelId = options?.ModelId ?? this._metadata.DefaultModelId + ?? throw new InvalidOperationException("No model ID specified in options or default model provided at the client initialization."); + + BetaMessage response = await this._client.Beta.Messages.Create(ChatClientHelper.CreateBetaMessageParameters(this, modelId, messages, options), cancellationToken).ConfigureAwait(false); + + ChatMessage chatMessage = new(ChatRole.Assistant, ChatClientHelper.ProcessResponseContent(response)); + + return new ChatResponse(chatMessage) + { + ResponseId = response.ID, + FinishReason = response.StopReason?.Value() switch + { + BetaStopReason.MaxTokens => ChatFinishReason.Length, + _ => ChatFinishReason.Stop, + }, + ModelId = response.Model, + RawRepresentation = response, + Usage = response.Usage is { } usage ? ChatClientHelper.CreateUsageDetails(usage) : null + }; + } + + public object? GetService(System.Type serviceType, object? serviceKey = null) + { + return (serviceKey is null && serviceType == typeof(AnthropicClient)) + ? this._client + : (serviceKey is null && serviceType == typeof(ChatClientMetadata)) + ? this._metadata + : null; + } + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} + +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(string))] +internal sealed partial class AnthropicClientJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs deleted file mode 100644 index dcc771a572..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicChatClient.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#pragma warning disable CA1812 - -using System.Text.Json; -using System.Text.Json.Serialization; -using Anthropic.Client; -using Anthropic.Client.Models.Beta.Messages; -using Anthropic.Client.Models.Messages; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Anthropic; - -/// -/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using -/// Azure-specific agent capabilities. -/// -internal sealed class AnthropicChatClient : IChatClient -{ - private readonly AnthropicClient _client; - private readonly ChatClientMetadata _metadata; - - internal AnthropicChatClient(AnthropicClient client, Uri? endpoint = null, string? defaultModelId = null) - { - this._client = client; - this._metadata = new ChatClientMetadata(providerName: "anthrendpointopic", providerUri: endpoint ?? new Uri("https://api.anthropic.com"), defaultModelId); - } - - public void Dispose() - { - } - - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - Message messageResponse = await this._client.Messages.Create(ChatClientHelper.CreateMessageParameters(this, messages, options)); - throw new NotImplementedException(); - } - - public object? GetService(Type serviceType, object? serviceKey = null) - { - return (serviceKey is null && serviceType == typeof(AnthropicClient)) - ? this._client - : (serviceKey is null && serviceType == typeof(ChatClientMetadata)) - ? this._metadata - : null; - } - - public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index 9fd745a423..381f06f8d0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -47,6 +47,6 @@ public static IChatClient AsIChatClient( this AnthropicClient client, string model) { - return new AnthropicChatClient(client); + return new AnthropicBetaChatClient(client, defaultModelId: model); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs index 0b0c32f837..383dc88db5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs @@ -5,7 +5,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Anthropic.Client.Models.Messages; +using Anthropic.Models.Beta.Messages; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Anthropic; @@ -18,7 +18,7 @@ internal static class ChatClientHelper /// /// Create usage details from usage /// - public static UsageDetails CreateUsageDetails(Usage usage) + public static UsageDetails CreateUsageDetails(BetaUsage usage) { UsageDetails usageDetails = new() { @@ -43,107 +43,124 @@ public static UsageDetails CreateUsageDetails(Usage usage) private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclaration function) => new(function.JsonSchema.EnumerateObject().ToDictionary(k => k.Name, v => v.Value)); - public static ThinkingConfigParam? GetThinkingParameters(this ChatOptions options) + public static BetaThinkingConfigParam? GetThinkingParameters(this ChatOptions options) { const string ThinkingParametersKey = "Anthropic.ThinkingParameters"; if (options?.AdditionalProperties?.TryGetValue(ThinkingParametersKey, out var value) == true) { - return value as ThinkingConfigParam; + return value as BetaThinkingConfigParam; } return null; } + public static List? GetMcpServers(ChatOptions? options) + { + List? mcpServerDefinitions = null; + + if (options?.Tools is { Count: > 0 }) + { +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + foreach (var mcpt in options.Tools.OfType()) + { + (mcpServerDefinitions ??= []).Add( + new BetaRequestMCPServerURLDefinition() + { + Name = mcpt.ServerName, + URL = mcpt.ServerAddress, + AuthorizationToken = mcpt.AuthorizationToken, + ToolConfiguration = new() { AllowedTools = mcpt.AllowedTools?.ToList() } + }); + } +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + return mcpServerDefinitions; + } + /// /// Create message parameters from chat messages and options /// - public static MessageCreateParams CreateMessageParameters(IChatClient client, IEnumerable messages, ChatOptions options) + internal static MessageCreateParams CreateBetaMessageParameters(IChatClient client, string modelId, IEnumerable messages, ChatOptions? options) { - if (options.RawRepresentationFactory?.Invoke(client) is not MessageCreateParams parameters) + if (options?.RawRepresentationFactory?.Invoke(client) is MessageCreateParams parameters && parameters is not null) { - List? tools = null; - ToolChoice? toolChoice = null; + return parameters; + } + + List? tools = null; + BetaToolChoice? toolChoice = null; - if (options.Tools is { Count: > 0 }) + if (options?.Tools is { Count: > 0 }) + { + if (options.ToolMode is RequiredChatToolMode r) { - if (options.ToolMode is RequiredChatToolMode r) - { - toolChoice = r.RequiredFunctionName is null ? new ToolChoice(new ToolChoiceAny()) : new ToolChoice(new ToolChoiceTool(r.RequiredFunctionName)); - } + toolChoice = r.RequiredFunctionName is null ? new BetaToolChoice(new BetaToolChoiceAny()) : new BetaToolChoice(new BetaToolChoiceTool(r.RequiredFunctionName)); + } - tools = []; - foreach (var tool in options.Tools) + tools = []; + foreach (var tool in options.Tools) + { + switch (tool) { - switch (tool) - { - case AIFunctionDeclaration f: - tools.Add(new ToolUnion(new Tool() - { - Name = f.Name, - Description = f.Description, - InputSchema = AIFunctionDeclarationToInputSchema(f) - })); - break; - - /* - case HostedCodeInterpreterTool: - tools.Add(new ToolUnion(CodeInterpreter)); - break; - case HostedWebSearchTool: - tools.Add(new ToolUnion(ServerTools.GetWebSearchTool(5))); - break; - #pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. - case HostedMcpServerTool mcpt: - MCPServer mcpServer = new() - { - Url = mcpt.ServerAddress, - Name = mcpt.ServerName, - }; - - if (mcpt.AllowedTools is not null) - { - mcpServer.ToolConfiguration.AllowedTools.AddRange(mcpt.AllowedTools); - } - - mcpServer.AuthorizationToken = mcpt.AuthorizationToken; + case AIFunctionDeclaration f: + tools.Add(new BetaToolUnion(new BetaTool() + { + Name = f.Name, + Description = f.Description, + InputSchema = AIFunctionDeclarationToInputSchema(f) + })); + break; + + case HostedCodeInterpreterTool codeTool: + if (codeTool.AdditionalProperties?["version"] is string version && version.Contains("20250522")) + { + tools.Add(new BetaCodeExecutionTool20250522()); + } + else + { + tools.Add(new BetaCodeExecutionTool20250825()); + } + break; - (parameters.MCPServers ??= []).Add(mcpServer); - break; -#pragma warning restore MEAI001 - */ - } + case HostedWebSearchTool webSearchTool: + tools.Add(new BetaToolUnion(new BetaWebSearchTool20250305() + { + MaxUses = (long?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.MaxUses)], + AllowedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.AllowedDomains)], + BlockedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.BlockedDomains)], + CacheControl = (BetaCacheControlEphemeral?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.CacheControl)], + Name = JsonSerializer.Deserialize(JsonSerializer.Serialize(webSearchTool.Name, AnthropicClientJsonContext.Default.String), AnthropicClientJsonContext.Default.JsonElement), + UserLocation = (UserLocation?)webSearchTool.AdditionalProperties?[nameof(UserLocation)] + })); + break; } } - - parameters = new MessageCreateParams() - { - Model = options.ModelId!, - Messages = GetMessages(messages), - System = GetSystem(options, messages), - MaxTokens = (options.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, - Temperature = (options.Temperature is float temperature) ? (double)temperature : null, - TopP = (options.TopP is float topP) ? (double)topP : null, - TopK = (options.TopK is int topK) ? topK : null, - StopSequences = (options.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : null, - ToolChoice = toolChoice, - Tools = tools, - Thinking = options.GetThinkingParameters(), - }; } - // Avoid errors from completely empty input. - if (!parameters.Messages.Any(m => m.Content.Count > 0)) + parameters = new MessageCreateParams() { - parameters.Messages.Add(new(RoleType.User, "\u200b")); // zero-width space - } + Model = modelId, + Messages = GetMessages(messages), + System = GetSystem(options, messages), + MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, + Temperature = (options?.Temperature is float temperature) ? (double)temperature : null, + TopP = (options?.TopP is float topP) ? (double)topP : null, + TopK = (options?.TopK is int topK) ? topK : null, + StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : null, + ToolChoice = toolChoice, + Tools = tools, + Thinking = options?.GetThinkingParameters(), + MCPServers = GetMcpServers(options), + }; return parameters; } - private static SystemModel? GetSystem(ChatOptions options, IEnumerable messages) + private static SystemModel? GetSystem(ChatOptions? options, IEnumerable messages) { - StringBuilder? fullInstructions = (options.Instructions is string instructions) ? new(instructions) : null; + StringBuilder? fullInstructions = (options?.Instructions is string instructions) ? new(instructions) : null; foreach (ChatMessage message in messages) { @@ -156,182 +173,334 @@ public static MessageCreateParams CreateMessageParameters(IChatClient client, IE return fullInstructions is not null ? new SystemModel(fullInstructions.ToString()) : null; } - private static List GetMessages(IEnumerable chatMessages) + private static List GetMessages(IEnumerable chatMessages) { - List messages = []; + List betaMessages = []; - foreach (ChatMessage chatMessage in chatMessages) + foreach (ChatMessage message in chatMessages) { - if (chatMessage.Role != ChatRole.System) + if (message.Role == ChatRole.System) { - // Process contents in order, creating new messages when switching between tool results and other content - // This preserves ordering and handles interleaved tool calls, AI output, and tool results - MessageParam? currentMessage = null; - bool lastWasToolResult = false; + continue; + } + + // Process contents in order, creating new messages when switching between tool results and other content + // This preserves ordering and handles interleaved tool calls, AI output, and tool results + BetaMessageParam? currentMessage = null; + bool lastWasToolResult = false; - foreach (AIContent content in chatMessage.Contents) + for (var currentIndex = 0; currentIndex < message.Contents.Count; currentIndex++) + { + bool isToolResult = message.Contents[currentIndex] is FunctionResultContent; + + // Create new message if: + // 1. This is the first content item, OR + // 2. We're switching between tool result and non-tool result content + if (currentMessage == null || lastWasToolResult != isToolResult) { - bool isToolResult = content is FunctionResultContent; + var messageRole = isToolResult ? Role.User : (message.Role == ChatRole.Assistant ? Role.Assistant : Role.User); + currentMessage = new() + { + // Tool results must always be in User messages, others respect original role + Role = messageRole, + Content = new BetaMessageParamContent(GetContents(message.Contents, currentIndex, messageRole)) + }; + betaMessages.Add(currentMessage); + lastWasToolResult = isToolResult; + } + } + } + + betaMessages.RemoveAll(m => m.Content.TryPickBetaContentBlockParams(out var blocks) && blocks.Count == 0); + + // Avoid errors from completely empty input. + if (betaMessages.Count == 0) + { + betaMessages.Add(new BetaMessageParam() { Role = Role.User, Content = "\u200b" }); // zero-width space + } + + return betaMessages; + } + + private static List GetContents(IList contents, int currentIndex, Role currentRole) + { + bool addedToolResult = false; + List contentBlocks = []; + for (var i = currentIndex; i < contents.Count; i++) + { + switch (contents[i]) + { + case FunctionResultContent frc: + if (addedToolResult) + { + // Any subsequent function result needs to be processed as a new message + goto end; + } + addedToolResult = true; + contentBlocks.Add(new BetaToolResultBlockParam(frc.CallId) + { + Content = new BetaToolResultBlockParamContent(frc.Result?.ToString() ?? string.Empty), + IsError = frc.Exception is not null, + CacheControl = frc.AdditionalProperties?[nameof(BetaToolResultBlockParam.CacheControl)] as BetaCacheControlEphemeral, + ToolUseID = frc.CallId, + }); + break; - // Create new message if: - // 1. This is the first content item, OR - // 2. We're switching between tool result and non-tool result content - if (currentMessage == null || lastWasToolResult != isToolResult) + case FunctionCallContent fcc: + contentBlocks.Add(new BetaToolUseBlockParam() { - currentMessage = new() + ID = fcc.CallId, + Name = fcc.Name, + Input = fcc.Arguments?.ToDictionary(k => k.Key, v => JsonSerializer.SerializeToElement(v.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? new Dictionary() + }); + break; + + case TextReasoningContent reasoningContent: + if (string.IsNullOrEmpty(reasoningContent.Text)) + { + contentBlocks.Add(new BetaRedactedThinkingBlockParam(reasoningContent.ProtectedData!)); + } + else + { + contentBlocks.Add(new BetaThinkingBlockParam() { - // Tool results must always be in User messages, others respect original role - Role = isToolResult ? RoleType.User : (chatMessage.Role == ChatRole.Assistant ? RoleType.Assistant : RoleType.User), - Content = new ContentModel(), - }; - messages.Add(currentMessage); - lastWasToolResult = isToolResult; + Signature = reasoningContent.ProtectedData!, + Thinking = reasoningContent.Text, + }); } + break; - // Add content to current message - switch (content) + case TextContent textContent: + string text = textContent.Text; + if (currentRole == Role.Assistant) { - case FunctionResultContent frc: - currentMessage.Content.Add(new ToolResultContent() - { - ToolUseId = frc.CallId, - Content = new List() { new TextContent() { Text = frc.Result?.ToString() ?? string.Empty } }, - IsError = frc.Exception is not null, - }); - break; - - case TextReasoningContent reasoningContent: - if (string.IsNullOrEmpty(reasoningContent.Text)) - { - currentMessage.Content.Add(new Messaging.RedactedThinkingContent() { Data = reasoningContent.ProtectedData }); - } - else - { - currentMessage.Content.Add(new Messaging.ThinkingContent() - { - Thinking = reasoningContent.Text, - Signature = reasoningContent.ProtectedData, - }); - } - break; + var trimmedText = text.TrimEnd(); + if (!string.IsNullOrEmpty(trimmedText)) + { + contentBlocks.Add(new BetaTextBlockParam() { Text = trimmedText }); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + contentBlocks.Add(new BetaTextBlockParam() { Text = text }); + } - case TextContent textContent: - string text = textContent.Text; - if (currentMessage.Role == RoleType.Assistant) - { - text.TrimEnd(); - if (!string.IsNullOrWhiteSpace(text)) - { - currentMessage.Content.Add(new Anthropic.Client.Models.MessagesTextContent() { Text = text }); - } - } - else if (!string.IsNullOrWhiteSpace(text)) - { - currentMessage.Content.Add(new TextContent() { Text = text }); - } + break; - break; + case HostedFileContent hostedFileContent: + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaFileDocumentSource(hostedFileContent.FileId))))); + break; - case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): - currentMessage.Content.Add(new ContentBlock() - { - Source = new() + case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): + contentBlocks.Add( + new BetaImageBlockParam( + new BetaImageBlockParamSource( + new BetaBase64ImageSource() { Data = Convert.ToBase64String(imageContent.Data.ToArray()), - MediaType = imageContent.MediaType, - } - }); - break; + MediaType = imageContent.MediaType + }))); + break; - case DataContent documentContent when documentContent.HasTopLevelMediaType("application"): - currentMessage.Content.Add(new DocumentContent() - { - Source = new() - { - Data = Convert.ToBase64String(documentContent.Data.ToArray()), - MediaType = documentContent.MediaType, - } - }); - break; + case DataContent pdfDocumentContent when pdfDocumentContent.MediaType == "application/pdf": + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaBase64PDFSource() + { + Data = Convert.ToBase64String(pdfDocumentContent.Data.ToArray()), + })))); + break; - case FunctionCallContent fcc: - currentMessage.Content.Add(new ToolUseContent() - { - Id = fcc.CallId, - Name = fcc.Name, - Input = JsonSerializer.SerializeToNode(fcc.Arguments) - }); - break; - } - } + case DataContent textDocumentContent when textDocumentContent.HasTopLevelMediaType("text"): + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaPlainTextSource() + { + Data = Convert.ToBase64String(textDocumentContent.Data.ToArray()), + })))); + break; + + case UriContent imageUriContent when imageUriContent.HasTopLevelMediaType("image"): + contentBlocks.Add( + new BetaImageBlockParam( + new BetaImageBlockParamSource( + new BetaURLImageSource(imageUriContent.Uri.ToString())))); + break; + + case UriContent pdfUriContent when pdfUriContent.MediaType == "application/pdf": + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaURLPDFSource(pdfUriContent.Uri.ToString()))))); + break; } } - parameters.Messages.RemoveAll(m => m.Content.Count == 0); +end: + return contentBlocks; } /// /// Process response content /// - public static List ProcessResponseContent(MessageResponse response) + public static List ProcessResponseContent(BetaMessage response) { List contents = new(); - foreach (ContentBase content in response.Content) + foreach (BetaContentBlock content in response.Content) { switch (content) { - case Messaging.ThinkingContent thinkingContent: - contents.Add(new TextReasoningContent(thinkingContent.Thinking) + case BetaContentBlock ct when ct.TryPickThinking(out var thinkingBlock): + contents.Add(new TextReasoningContent(thinkingBlock.Thinking) { - ProtectedData = thinkingContent.Signature, + ProtectedData = thinkingBlock.Signature, }); break; - case Messaging.RedactedThinkingContent redactedThinkingContent: + case BetaContentBlock ct when ct.TryPickRedactedThinking(out var redactedThinkingBlock): contents.Add(new TextReasoningContent(null) { - ProtectedData = redactedThinkingContent.Data, + ProtectedData = redactedThinkingBlock.Data, }); break; - case TextContent tc: - var textContent = new TextContent(tc.Text); - if (tc.Citations != null && tc.Citations.Any()) + case BetaContentBlock ct when ct.TryPickText(out var textBlock): + var textContent = new TextContent(textBlock.Text); + if (textBlock.Citations is { Count: > 0 }) { - foreach (var tau in tc.Citations) + foreach (var tau in textBlock.Citations) { - (textContent.Annotations ?? []).Add(new CitationAnnotation + var annotation = new CitationAnnotation() { RawRepresentation = tau, - AnnotatedRegions = - [ - new TextSpanAnnotatedRegion - { StartIndex = (int?)tau.StartPageNumber, EndIndex = (int?)tau.EndPageNumber } - ], - FileId = tau.Title - }); + Snippet = tau.CitedText, + FileId = tau.Title, + AnnotatedRegions = [] + }; + + switch (tau) + { + case BetaTextCitation bChar when bChar.TryPickCitationCharLocation(out var charLocation): + { + annotation.AnnotatedRegions.Add(new TextSpanAnnotatedRegion { StartIndex = (int?)charLocation?.StartCharIndex, EndIndex = (int?)charLocation?.EndCharIndex }); + break; + } + + case BetaTextCitation search when search.TryPickCitationSearchResultLocation(out var searchLocation) && Uri.IsWellFormedUriString(searchLocation.Source, UriKind.RelativeOrAbsolute): + { + annotation.Url = new Uri(searchLocation.Source); + break; + } + + case BetaTextCitation search when search.TryPickCitationsWebSearchResultLocation(out var searchLocation): + { + annotation.Url = new Uri(searchLocation.URL); + break; + } + + default: + { + (textContent.Annotations ?? []).Add(new CitationAnnotation + { + Snippet = tau.CitedText, + Title = tau.Title, + RawRepresentation = tau + }); + break; + } + } + + (textContent.Annotations ??= []).Add(annotation); } } contents.Add(textContent); break; - case ImageContent ic: - contents.Add(new DataContent(ic.Source.Data, ic.Source.MediaType)); + case BetaContentBlock ct when ct.TryPickToolUse(out var toolUse): + contents.Add(new FunctionCallContent(toolUse.ID, toolUse.Name) + { + Arguments = toolUse.Input?.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), + RawRepresentation = toolUse + }); + break; + + case BetaContentBlock ct when ct.TryPickMCPToolUse(out var mcpToolUse): +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + contents.Add(new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) + { + Arguments = mcpToolUse.Input.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), + RawRepresentation = mcpToolUse + }); + break; + + case BetaContentBlock ct when ct.TryPickMCPToolResult(out var mcpToolResult): + { + contents.Add(new McpServerToolResultContent(mcpToolResult.ToolUseID) + { + Output = [mcpToolResult.IsError + ? new ErrorContent(mcpToolResult.Content.Value.ToString()) + : new TextContent(mcpToolResult.Content.Value.ToString())], + RawRepresentation = mcpToolResult + }); + break; + } + + case BetaContentBlock ct when ct.TryPickCodeExecutionToolResult(out var cer): + { + var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; + if (cer.Content.TryPickError(out var cerErr)) + { + codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = cerErr.ErrorCode.Value().ToString() }); + } + if (cer.Content.TryPickResultBlock(out var cerResult)) + { + codeResult.Outputs.Add(new TextContent(cerResult.Stdout) { RawRepresentation = cerResult }); + if (!string.IsNullOrWhiteSpace(cerResult.Stderr)) + { + codeResult.Outputs.Add(new TextContent(cerResult.Stderr) { RawRepresentation = cerResult }); + } + } + + contents.Add(codeResult); break; + } - case ToolUseContent tuc: - contents.Add(new FunctionCallContent( - tuc.Id, - tuc.Name, - tuc.Input is not null ? tuc.Input.Deserialize>() : null)); + case BetaContentBlock ct when ct.TryPickBashCodeExecutionToolResult(out var bashCer): + { + var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; + if (bashCer.Content.TryPickBetaBashCodeExecutionToolResultError(out var bashCerErr)) + { + codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = bashCerErr.ErrorCode.Value().ToString() }); + } + if (bashCer.Content.TryPickBetaBashCodeExecutionResultBlock(out var bashCerResult)) + { + codeResult.Outputs.Add(new TextContent(bashCerResult.Stdout) { RawRepresentation = bashCerResult }); + if (!string.IsNullOrWhiteSpace(bashCerResult.Stderr)) + { + codeResult.Outputs.Add(new TextContent(bashCerResult.Stderr) { RawRepresentation = bashCerResult }); + } + } + + contents.Add(codeResult); break; + } +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - case ToolResultContent trc: - contents.Add(new FunctionResultContent( - trc.ToolUseId, - trc.Content)); + default: + { + contents.Add(new AIContent { RawRepresentation = content }); break; + } } } 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 index d8bf0b0a4b..1bdf8aa237 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -1,8 +1,7 @@ - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) + net9.0 preview enable true @@ -11,11 +10,11 @@ - + From c849515872bf630de9c7a2ba1ab446b8a2cc743e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:20:57 +0000 Subject: [PATCH 04/24] Update Thinking sample --- dotnet/agent-framework-dotnet.slnx | 4 ++ .../Agent_Anthropic_Step01_Running.csproj | 15 ++++++ .../Agent_Anthropic_Step01_Running/Program.cs | 25 +++++++++ .../Agent_Anthropic_Step02_Reasoning.csproj | 15 ++++++ .../Program.cs | 53 +++++++++++++++++++ .../AgentWithAnthropic/README.md | 14 +++++ .../AnthropicBetaChatClient.cs | 5 +- .../AnthropicClientExtensions.cs | 19 +++++-- .../ChatClientHelper.cs | 35 +++++------- 9 files changed, 158 insertions(+), 27 deletions(-) create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 445016fb51..39409cde52 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -82,6 +82,10 @@ + + + + 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..e1d991ce96 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Agent_Anthropic_Step01_Running.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.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..8dea9a81bf --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use a simple AI agent with OpenAI as the backend. + +using Anthropic; +using Anthropic.Client; +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_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..949c60146e --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Agent_Anthropic_Step02_Reasoning.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.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..cd1bbaab12 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to create and use an AI agent with reasoning capabilities. + +using Anthropic; +using Anthropic.Client; +using Anthropic.Core; +using Anthropic.Models.Beta.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 client = new AnthropicClient(new ClientOptions { APIKey = apiKey }).AsIChatClient(model).AsBuilder() + .ConfigureOptions( + o => (o.AdditionalProperties ??= []) + .Add(nameof(BetaThinkingConfigParam), new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(budgetTokens: 2048)))) + .Build(); + +AIAgent agent = new ChatClientAgent(client); + +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(string.Join("\n", response.Messages.SelectMany(m => m.Contents.OfType().Select(c => c.Text)))); +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.Write($"\e[97m{reasoningContent.Text}\e[0m"); + } + else if (item is TextContent textContent) + { + Console.Write(textContent.Text); + } + } +} +*/ diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md new file mode 100644 index 0000000000..4ed609ae81 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md @@ -0,0 +1,14 @@ +# Agent Framework with OpenAI + +These samples show how to use the Agent Framework with the OpenAI exchange types. + +By default, the .Net version of Agent Framework uses the [Microsoft.Extensions.AI.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/) exchange types. + +For developers who are using the [OpenAI SDK](https://www.nuget.org/packages/OpenAI) this can be problematic because there are conflicting exchange types which can cause confusion. + +Agent Framework provides additional support to allow OpenAI developers to use the OpenAI exchange types. + +|Sample|Description| +|---|---| +|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent instructions with native OpenAI SDK types.| + diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs index a605b0b575..3930ae3e2a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs @@ -19,12 +19,15 @@ internal sealed class AnthropicBetaChatClient : IChatClient private readonly AnthropicClient _client; private readonly ChatClientMetadata _metadata; - internal AnthropicBetaChatClient(AnthropicClient client, Uri? endpoint = null, string? defaultModelId = null) + internal AnthropicBetaChatClient(AnthropicClient client, long defaultMaxTokens, Uri? endpoint = null, string? defaultModelId = null) { this._client = client; this._metadata = new ChatClientMetadata(providerName: "anthropic", providerUri: endpoint ?? new Uri("https://api.anthropic.com"), defaultModelId); + this.DefaultMaxTokens = defaultMaxTokens; } + public long DefaultMaxTokens { get; set; } + public void Dispose() { } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index 381f06f8d0..8d639299a1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -11,6 +11,11 @@ namespace Anthropic.Client; /// public static class AnthropicClientExtensions { + /// + /// Specifies the default maximum number of tokens allowed for processing operations. + /// + public static long DefaultMaxTokens { get; set; } = 4096; + /// /// Creates a new AI agent using the specified model and options. /// @@ -19,13 +24,15 @@ public static class AnthropicClientExtensions /// The instructions for the AI agent. /// The name of the AI agent. /// The description of the AI agent. + /// The default maximum tokens for chat completions. Defaults to if not provided. /// The created AI agent. public static ChatClientAgent CreateAIAgent( this AnthropicClient client, string model, string? instructions, string? name = null, - string? description = null) + string? description = null, + long? defaultMaxTokens = null) { var options = new ChatClientAgentOptions { @@ -34,19 +41,21 @@ public static ChatClientAgent CreateAIAgent( Description = description, }; - return new ChatClientAgent(client.AsIChatClient(model), options); + return new ChatClientAgent(client.AsIChatClient(model, defaultMaxTokens), options); } /// /// Get an compatible implementation around the . /// /// The Anthropic client. - /// The model to use for chat completions. + /// The default model to use for chat completions. + /// The default maximum tokens for chat completions. Defaults to if not provided. /// The implementation. public static IChatClient AsIChatClient( this AnthropicClient client, - string model) + string defaultModelId, + long? defaultMaxTokens = null) { - return new AnthropicBetaChatClient(client, defaultModelId: model); + return new AnthropicBetaChatClient(client, defaultMaxTokens ?? DefaultMaxTokens, defaultModelId: defaultModelId); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs index 383dc88db5..cbd40eed40 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs @@ -45,9 +45,7 @@ private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclarat public static BetaThinkingConfigParam? GetThinkingParameters(this ChatOptions options) { - const string ThinkingParametersKey = "Anthropic.ThinkingParameters"; - - if (options?.AdditionalProperties?.TryGetValue(ThinkingParametersKey, out var value) == true) + if (options?.AdditionalProperties?.TryGetValue(nameof(BetaThinkingConfigParam), out var value) == true) { return value as BetaThinkingConfigParam; } @@ -82,13 +80,8 @@ private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclarat /// /// Create message parameters from chat messages and options /// - internal static MessageCreateParams CreateBetaMessageParameters(IChatClient client, string modelId, IEnumerable messages, ChatOptions? options) + internal static MessageCreateParams CreateBetaMessageParameters(AnthropicBetaChatClient client, string modelId, IEnumerable messages, ChatOptions? options) { - if (options?.RawRepresentationFactory?.Invoke(client) is MessageCreateParams parameters && parameters is not null) - { - return parameters; - } - List? tools = null; BetaToolChoice? toolChoice = null; @@ -139,23 +132,23 @@ internal static MessageCreateParams CreateBetaMessageParameters(IChatClient clie } } - parameters = new MessageCreateParams() + MessageCreateParams? providedParameters = options?.RawRepresentationFactory?.Invoke(client) as MessageCreateParams; + + return new MessageCreateParams() { Model = modelId, Messages = GetMessages(messages), System = GetSystem(options, messages), - MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : 4096, - Temperature = (options?.Temperature is float temperature) ? (double)temperature : null, - TopP = (options?.TopP is float topP) ? (double)topP : null, - TopK = (options?.TopK is int topK) ? topK : null, - StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : null, - ToolChoice = toolChoice, - Tools = tools, - Thinking = options?.GetThinkingParameters(), - MCPServers = GetMcpServers(options), + MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : providedParameters?.MaxTokens ?? client.DefaultMaxTokens, + Temperature = (options?.Temperature is float temperature) ? (double)temperature : providedParameters?.Temperature, + TopP = (options?.TopP is float topP) ? (double)topP : providedParameters?.TopP, + TopK = (options?.TopK is int topK) ? topK : providedParameters?.TopK, + StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : providedParameters?.StopSequences, + ToolChoice = toolChoice ?? providedParameters?.ToolChoice, + Tools = tools ?? providedParameters?.Tools, + Thinking = options?.GetThinkingParameters() ?? providedParameters?.Thinking, + MCPServers = GetMcpServers(options) ?? providedParameters?.MCPServers, }; - - return parameters; } private static SystemModel? GetSystem(ChatOptions? options, IEnumerable messages) From 6e6e54783cf781ea32d0243148ff0ac8764600a8 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:31:17 +0000 Subject: [PATCH 05/24] Non-Streaming Function calling working --- dotnet/agent-framework-dotnet.slnx | 1 + ...Anthropic_Step03_UsingFunctionTools.csproj | 15 + .../Program.cs | 39 ++ .../README.md | 48 ++ .../AnthropicBetaChatClient.cs | 514 ++++++++++++++++- .../AnthropicClientExtensions.cs | 34 +- .../ChatClientHelper.cs | 517 ------------------ 7 files changed, 647 insertions(+), 521 deletions(-) create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md delete mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 39409cde52..70cb9a9f66 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -85,6 +85,7 @@ + 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..9c89136316 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Agent_Anthropic_Step03_UsingFunctionTools.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.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..6e542214f4 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs @@ -0,0 +1,39 @@ +// 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..934373aa80 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md @@ -0,0 +1,48 @@ +# Using Function Tools with AI Agents + +This sample demonstrates how to use function tools with AI agents, allowing agents to call custom functions to retrieve information. + +## What this sample demonstrates + +- Creating function tools using AIFunctionFactory +- Passing function tools to an AI agent +- Running agents with function tools (text output) +- Running agents with function tools (streaming output) +- Managing agent lifecycle (creation and deletion) + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Azure Foundry service endpoint and deployment configured +- Azure CLI installed and authenticated (for Azure credential authentication) + +**Note**: This demo uses Azure CLI credentials for authentication. 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). + +Set the following environment variables: + +```powershell +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint +$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +``` + +## Run the sample + +Navigate to the FoundryAgents sample directory and run: + +```powershell +cd dotnet/samples/GettingStarted/FoundryAgents +dotnet run --project .\FoundryAgents_Step03.1_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/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs index 3930ae3e2a..1ad9e14340 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs @@ -2,11 +2,13 @@ #pragma warning disable CA1812 +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Anthropic; using Anthropic.Models.Beta.Messages; using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Anthropic; @@ -37,9 +39,9 @@ public async Task GetResponseAsync(IEnumerable messag var modelId = options?.ModelId ?? this._metadata.DefaultModelId ?? throw new InvalidOperationException("No model ID specified in options or default model provided at the client initialization."); - BetaMessage response = await this._client.Beta.Messages.Create(ChatClientHelper.CreateBetaMessageParameters(this, modelId, messages, options), cancellationToken).ConfigureAwait(false); + BetaMessage response = await this._client.Beta.Messages.Create(CreateBetaMessageParameters(this, modelId, messages, options), cancellationToken).ConfigureAwait(false); - ChatMessage chatMessage = new(ChatRole.Assistant, ChatClientHelper.ProcessResponseContent(response)); + ChatMessage chatMessage = new(ChatRole.Assistant, ProcessResponseContent(response)); return new ChatResponse(chatMessage) { @@ -51,7 +53,7 @@ public async Task GetResponseAsync(IEnumerable messag }, ModelId = response.Model, RawRepresentation = response, - Usage = response.Usage is { } usage ? ChatClientHelper.CreateUsageDetails(usage) : null + Usage = response.Usage is { } usage ? CreateUsageDetails(usage) : null }; } @@ -68,6 +70,512 @@ public IAsyncEnumerable GetStreamingResponseAsync(IEnumerabl { throw new NotImplementedException(); } + + /// Provides an wrapper for a . + internal sealed class BetaToolAITool(BetaTool tool) : AITool + { + public BetaTool Tool => tool; + public override string Name => this.Tool.GetType().Name; + + /// + public override object? GetService(System.Type serviceType, object? serviceKey = null) + { + _ = Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(this.Tool) ? this.Tool : + base.GetService(serviceType, serviceKey); + } + } + + /// + /// Create usage details from usage + /// + private static UsageDetails CreateUsageDetails(BetaUsage usage) + { + UsageDetails usageDetails = new() + { + InputTokenCount = usage.InputTokens, + OutputTokenCount = usage.OutputTokens, + AdditionalCounts = [], + }; + + if (usage.CacheCreationInputTokens.HasValue) + { + usageDetails.AdditionalCounts.Add(nameof(usage.CacheCreationInputTokens), usage.CacheCreationInputTokens.Value); + } + + if (usage.CacheReadInputTokens.HasValue) + { + usageDetails.AdditionalCounts.Add(nameof(usage.CacheReadInputTokens), usage.CacheReadInputTokens.Value); + } + + return usageDetails; + } + + private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclaration function) + => new(function.JsonSchema.EnumerateObject().ToDictionary(k => k.Name, v => v.Value)); + + private static BetaThinkingConfigParam? GetThinkingParameters(ChatOptions? options) + { + if (options?.AdditionalProperties?.TryGetValue(nameof(BetaThinkingConfigParam), out var value) == true) + { + return value as BetaThinkingConfigParam; + } + + return null; + } + + private static List? GetMcpServers(ChatOptions? options) + { + List? mcpServerDefinitions = null; + + if (options?.Tools is { Count: > 0 }) + { +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + foreach (var mcpt in options.Tools.OfType()) + { + (mcpServerDefinitions ??= []).Add( + new BetaRequestMCPServerURLDefinition() + { + Name = mcpt.ServerName, + URL = mcpt.ServerAddress, + AuthorizationToken = mcpt.AuthorizationToken, + ToolConfiguration = new() { AllowedTools = mcpt.AllowedTools?.ToList() } + }); + } +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + return mcpServerDefinitions; + } + + /// + /// Create message parameters from chat messages and options + /// + private static MessageCreateParams CreateBetaMessageParameters(AnthropicBetaChatClient client, string modelId, IEnumerable messages, ChatOptions? options) + { + List? tools = null; + BetaToolChoice? toolChoice = null; + + if (options?.Tools is { Count: > 0 }) + { + if (options.ToolMode is RequiredChatToolMode r) + { + toolChoice = r.RequiredFunctionName is null ? new BetaToolChoice(new BetaToolChoiceAny()) : new BetaToolChoice(new BetaToolChoiceTool(r.RequiredFunctionName)); + } + + tools = []; + foreach (var tool in options.Tools) + { + switch (tool) + { + case BetaToolAITool betaToolAiTool: + tools.Add(new BetaToolUnion(betaToolAiTool.Tool)); + break; + + case AIFunctionDeclaration f: + tools.Add(new BetaToolUnion(new BetaTool() + { + Name = f.Name, + Description = f.Description, + InputSchema = AIFunctionDeclarationToInputSchema(f) + })); + break; + + case HostedCodeInterpreterTool codeTool: + if (codeTool.AdditionalProperties?["version"] is string version && version.Contains("20250522")) + { + tools.Add(new BetaCodeExecutionTool20250522()); + } + else + { + tools.Add(new BetaCodeExecutionTool20250825()); + } + break; + + case HostedWebSearchTool webSearchTool: + tools.Add(new BetaToolUnion(new BetaWebSearchTool20250305() + { + MaxUses = (long?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.MaxUses)], + AllowedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.AllowedDomains)], + BlockedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.BlockedDomains)], + CacheControl = (BetaCacheControlEphemeral?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.CacheControl)], + Name = JsonSerializer.Deserialize(JsonSerializer.Serialize(webSearchTool.Name, AnthropicClientJsonContext.Default.String), AnthropicClientJsonContext.Default.JsonElement), + UserLocation = (UserLocation?)webSearchTool.AdditionalProperties?[nameof(UserLocation)] + })); + break; + } + } + } + + MessageCreateParams? providedParameters = options?.RawRepresentationFactory?.Invoke(client) as MessageCreateParams; + + return new MessageCreateParams() + { + Model = modelId, + Messages = GetMessages(messages), + System = GetSystem(options, messages), + MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : providedParameters?.MaxTokens ?? client.DefaultMaxTokens, + Temperature = (options?.Temperature is float temperature) ? (double)temperature : providedParameters?.Temperature, + TopP = (options?.TopP is float topP) ? (double)topP : providedParameters?.TopP, + TopK = (options?.TopK is int topK) ? topK : providedParameters?.TopK, + StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : providedParameters?.StopSequences, + ToolChoice = toolChoice ?? providedParameters?.ToolChoice, + Tools = tools ?? providedParameters?.Tools, + Thinking = GetThinkingParameters(options) ?? providedParameters?.Thinking, + MCPServers = GetMcpServers(options) ?? providedParameters?.MCPServers, + }; + } + + private static SystemModel? GetSystem(ChatOptions? options, IEnumerable messages) + { + StringBuilder? fullInstructions = (options?.Instructions is string instructions) ? new(instructions) : null; + + foreach (ChatMessage message in messages) + { + if (message.Role == ChatRole.System) + { + (fullInstructions ??= new()).AppendLine(string.Concat(message.Contents.OfType())); + } + } + + return fullInstructions is not null ? new SystemModel(fullInstructions.ToString()) : null; + } + + private static List GetMessages(IEnumerable chatMessages) + { + List betaMessages = []; + + foreach (ChatMessage message in chatMessages) + { + if (message.Role == ChatRole.System) + { + continue; + } + + // Process contents in order, creating new messages when switching between tool results and other content + // This preserves ordering and handles interleaved tool calls, AI output, and tool results + BetaMessageParam? currentMessage = null; + bool lastWasToolResult = false; + + for (var currentIndex = 0; currentIndex < message.Contents.Count; currentIndex++) + { + bool isToolResult = message.Contents[currentIndex] is FunctionResultContent; + + // Create new message if: + // 1. This is the first content item, OR + // 2. We're switching between tool result and non-tool result content + if (currentMessage == null || lastWasToolResult != isToolResult) + { + var messageRole = isToolResult ? Role.User : (message.Role == ChatRole.Assistant ? Role.Assistant : Role.User); + currentMessage = new() + { + // Tool results must always be in User messages, others respect original role + Role = messageRole, + Content = new BetaMessageParamContent(GetContents(message.Contents, currentIndex, messageRole)) + }; + betaMessages.Add(currentMessage); + lastWasToolResult = isToolResult; + } + } + } + + betaMessages.RemoveAll(m => m.Content.TryPickBetaContentBlockParams(out var blocks) && blocks.Count == 0); + + // Avoid errors from completely empty input. + if (betaMessages.Count == 0) + { + betaMessages.Add(new BetaMessageParam() { Role = Role.User, Content = "\u200b" }); // zero-width space + } + + return betaMessages; + } + + private static List GetContents(IList contents, int currentIndex, Role currentRole) + { + bool addedToolResult = false; + List contentBlocks = []; + for (var i = currentIndex; i < contents.Count; i++) + { + switch (contents[i]) + { + case FunctionResultContent frc: + if (addedToolResult) + { + // Any subsequent function result needs to be processed as a new message + goto end; + } + addedToolResult = true; + contentBlocks.Add(new BetaToolResultBlockParam(frc.CallId) + { + Content = new BetaToolResultBlockParamContent(frc.Result?.ToString() ?? string.Empty), + IsError = frc.Exception is not null, + CacheControl = frc.AdditionalProperties?[nameof(BetaToolResultBlockParam.CacheControl)] as BetaCacheControlEphemeral, + ToolUseID = frc.CallId, + }); + break; + + case FunctionCallContent fcc: + contentBlocks.Add(new BetaToolUseBlockParam() + { + ID = fcc.CallId, + Name = fcc.Name, + Input = fcc.Arguments?.ToDictionary(k => k.Key, v => JsonSerializer.SerializeToElement(v.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? new Dictionary() + }); + break; + + case TextReasoningContent reasoningContent: + if (string.IsNullOrEmpty(reasoningContent.Text)) + { + contentBlocks.Add(new BetaRedactedThinkingBlockParam(reasoningContent.ProtectedData!)); + } + else + { + contentBlocks.Add(new BetaThinkingBlockParam() + { + Signature = reasoningContent.ProtectedData!, + Thinking = reasoningContent.Text, + }); + } + break; + + case TextContent textContent: + string text = textContent.Text; + if (currentRole == Role.Assistant) + { + var trimmedText = text.TrimEnd(); + if (!string.IsNullOrEmpty(trimmedText)) + { + contentBlocks.Add(new BetaTextBlockParam() { Text = trimmedText }); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + contentBlocks.Add(new BetaTextBlockParam() { Text = text }); + } + + break; + + case HostedFileContent hostedFileContent: + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaFileDocumentSource(hostedFileContent.FileId))))); + break; + + case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): + contentBlocks.Add( + new BetaImageBlockParam( + new BetaImageBlockParamSource( + new BetaBase64ImageSource() + { + Data = Convert.ToBase64String(imageContent.Data.ToArray()), + MediaType = imageContent.MediaType + }))); + break; + + case DataContent pdfDocumentContent when pdfDocumentContent.MediaType == "application/pdf": + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaBase64PDFSource() + { + Data = Convert.ToBase64String(pdfDocumentContent.Data.ToArray()), + })))); + break; + + case DataContent textDocumentContent when textDocumentContent.HasTopLevelMediaType("text"): + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaPlainTextSource() + { + Data = Convert.ToBase64String(textDocumentContent.Data.ToArray()), + })))); + break; + + case UriContent imageUriContent when imageUriContent.HasTopLevelMediaType("image"): + contentBlocks.Add( + new BetaImageBlockParam( + new BetaImageBlockParamSource( + new BetaURLImageSource(imageUriContent.Uri.ToString())))); + break; + + case UriContent pdfUriContent when pdfUriContent.MediaType == "application/pdf": + contentBlocks.Add( + new BetaContentBlockParam( + new BetaRequestDocumentBlock( + new BetaRequestDocumentBlockSource( + new BetaURLPDFSource(pdfUriContent.Uri.ToString()))))); + break; + } + } + +end: + return contentBlocks; + } + + /// + /// Process response content + /// + private static List ProcessResponseContent(BetaMessage response) + { + List contents = new(); + + foreach (BetaContentBlock content in response.Content) + { + switch (content) + { + case BetaContentBlock ct when ct.TryPickThinking(out var thinkingBlock): + contents.Add(new TextReasoningContent(thinkingBlock.Thinking) + { + ProtectedData = thinkingBlock.Signature, + }); + break; + + case BetaContentBlock ct when ct.TryPickRedactedThinking(out var redactedThinkingBlock): + contents.Add(new TextReasoningContent(null) + { + ProtectedData = redactedThinkingBlock.Data, + }); + break; + + case BetaContentBlock ct when ct.TryPickText(out var textBlock): + var textContent = new TextContent(textBlock.Text); + if (textBlock.Citations is { Count: > 0 }) + { + foreach (var tau in textBlock.Citations) + { + var annotation = new CitationAnnotation() + { + RawRepresentation = tau, + Snippet = tau.CitedText, + FileId = tau.Title, + AnnotatedRegions = [] + }; + + switch (tau) + { + case BetaTextCitation bChar when bChar.TryPickCitationCharLocation(out var charLocation): + { + annotation.AnnotatedRegions.Add(new TextSpanAnnotatedRegion { StartIndex = (int?)charLocation?.StartCharIndex, EndIndex = (int?)charLocation?.EndCharIndex }); + break; + } + + case BetaTextCitation search when search.TryPickCitationSearchResultLocation(out var searchLocation) && Uri.IsWellFormedUriString(searchLocation.Source, UriKind.RelativeOrAbsolute): + { + annotation.Url = new Uri(searchLocation.Source); + break; + } + + case BetaTextCitation search when search.TryPickCitationsWebSearchResultLocation(out var searchLocation): + { + annotation.Url = new Uri(searchLocation.URL); + break; + } + + default: + { + (textContent.Annotations ?? []).Add(new CitationAnnotation + { + Snippet = tau.CitedText, + Title = tau.Title, + RawRepresentation = tau + }); + break; + } + } + + (textContent.Annotations ??= []).Add(annotation); + } + } + contents.Add(textContent); + break; + + case BetaContentBlock ct when ct.TryPickToolUse(out var toolUse): + contents.Add(new FunctionCallContent(toolUse.ID, toolUse.Name) + { + Arguments = toolUse.Input?.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), + RawRepresentation = toolUse + }); + break; + + case BetaContentBlock ct when ct.TryPickMCPToolUse(out var mcpToolUse): +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + contents.Add(new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) + { + Arguments = mcpToolUse.Input.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), + RawRepresentation = mcpToolUse + }); + break; + + case BetaContentBlock ct when ct.TryPickMCPToolResult(out var mcpToolResult): + { + contents.Add(new McpServerToolResultContent(mcpToolResult.ToolUseID) + { + Output = [mcpToolResult.IsError + ? new ErrorContent(mcpToolResult.Content.Value.ToString()) + : new TextContent(mcpToolResult.Content.Value.ToString())], + RawRepresentation = mcpToolResult + }); + break; + } + + case BetaContentBlock ct when ct.TryPickCodeExecutionToolResult(out var cer): + { + var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; + if (cer.Content.TryPickError(out var cerErr)) + { + codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = cerErr.ErrorCode.Value().ToString() }); + } + if (cer.Content.TryPickResultBlock(out var cerResult)) + { + codeResult.Outputs.Add(new TextContent(cerResult.Stdout) { RawRepresentation = cerResult }); + if (!string.IsNullOrWhiteSpace(cerResult.Stderr)) + { + codeResult.Outputs.Add(new TextContent(cerResult.Stderr) { RawRepresentation = cerResult }); + } + } + + contents.Add(codeResult); + break; + } + + case BetaContentBlock ct when ct.TryPickBashCodeExecutionToolResult(out var bashCer): + { + var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; + if (bashCer.Content.TryPickBetaBashCodeExecutionToolResultError(out var bashCerErr)) + { + codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = bashCerErr.ErrorCode.Value().ToString() }); + } + if (bashCer.Content.TryPickBetaBashCodeExecutionResultBlock(out var bashCerResult)) + { + codeResult.Outputs.Add(new TextContent(bashCerResult.Stdout) { RawRepresentation = bashCerResult }); + if (!string.IsNullOrWhiteSpace(bashCerResult.Stderr)) + { + codeResult.Outputs.Add(new TextContent(bashCerResult.Stderr) { RawRepresentation = bashCerResult }); + } + } + + contents.Add(codeResult); + break; + } +#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + default: + { + contents.Add(new AIContent { RawRepresentation = content }); + break; + } + } + } + + return contents; + } } [JsonSerializable(typeof(JsonElement))] diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index 8d639299a1..1dfdbda0ad 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using Anthropic.Models.Beta.Messages; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Anthropic; using Microsoft.Extensions.AI; -namespace Anthropic.Client; +namespace Anthropic; /// /// Provides extension methods for the class. @@ -24,6 +25,7 @@ public static class AnthropicClientExtensions /// 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. /// The created AI agent. public static ChatClientAgent CreateAIAgent( @@ -32,6 +34,7 @@ public static ChatClientAgent CreateAIAgent( string? instructions, string? name = null, string? description = null, + IList? tools = null, long? defaultMaxTokens = null) { var options = new ChatClientAgentOptions @@ -41,6 +44,11 @@ public static ChatClientAgent CreateAIAgent( Description = description, }; + if (tools is { Count: > 0 }) + { + options.ChatOptions = new ChatOptions { Tools = tools }; + } + return new ChatClientAgent(client.AsIChatClient(model, defaultMaxTokens), options); } @@ -58,4 +66,28 @@ public static IChatClient AsIChatClient( { return new AnthropicBetaChatClient(client, defaultMaxTokens ?? DefaultMaxTokens, defaultModelId: defaultModelId); } + + /// Creates an to represent a raw . + /// The tool to wrap as an . + /// The wrapped as an . + /// + /// + /// The returned tool is only suitable for use with the returned by + /// (or s that delegate + /// to such an instance). It is likely to be ignored by any other implementation. + /// + /// + /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, + /// such as , , , or + /// , those types should be preferred instead of this method, as they are more portable, + /// capable of being respected by any implementation. This method does not attempt to + /// map the supplied to any of those types, it simply wraps it as-is: + /// the returned by will + /// be able to unwrap the when it processes the list of tools. + /// + /// + public static AITool AsAITool(this BetaTool tool) + { + return new AnthropicBetaChatClient.BetaToolAITool(tool); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs deleted file mode 100644 index cbd40eed40..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/ChatClientHelper.cs +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#pragma warning disable CA1812 - -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Anthropic.Models.Beta.Messages; -using Microsoft.Extensions.AI; - -namespace Microsoft.Agents.AI.Anthropic; - -/// -/// Helper class for chat client implementations -/// -internal static class ChatClientHelper -{ - /// - /// Create usage details from usage - /// - public static UsageDetails CreateUsageDetails(BetaUsage usage) - { - UsageDetails usageDetails = new() - { - InputTokenCount = usage.InputTokens, - OutputTokenCount = usage.OutputTokens, - AdditionalCounts = [], - }; - - if (usage.CacheCreationInputTokens.HasValue) - { - usageDetails.AdditionalCounts.Add(nameof(usage.CacheCreationInputTokens), usage.CacheCreationInputTokens.Value); - } - - if (usage.CacheReadInputTokens.HasValue) - { - usageDetails.AdditionalCounts.Add(nameof(usage.CacheReadInputTokens), usage.CacheReadInputTokens.Value); - } - - return usageDetails; - } - - private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclaration function) - => new(function.JsonSchema.EnumerateObject().ToDictionary(k => k.Name, v => v.Value)); - - public static BetaThinkingConfigParam? GetThinkingParameters(this ChatOptions options) - { - if (options?.AdditionalProperties?.TryGetValue(nameof(BetaThinkingConfigParam), out var value) == true) - { - return value as BetaThinkingConfigParam; - } - - return null; - } - - public static List? GetMcpServers(ChatOptions? options) - { - List? mcpServerDefinitions = null; - - if (options?.Tools is { Count: > 0 }) - { -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - foreach (var mcpt in options.Tools.OfType()) - { - (mcpServerDefinitions ??= []).Add( - new BetaRequestMCPServerURLDefinition() - { - Name = mcpt.ServerName, - URL = mcpt.ServerAddress, - AuthorizationToken = mcpt.AuthorizationToken, - ToolConfiguration = new() { AllowedTools = mcpt.AllowedTools?.ToList() } - }); - } -#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - return mcpServerDefinitions; - } - - /// - /// Create message parameters from chat messages and options - /// - internal static MessageCreateParams CreateBetaMessageParameters(AnthropicBetaChatClient client, string modelId, IEnumerable messages, ChatOptions? options) - { - List? tools = null; - BetaToolChoice? toolChoice = null; - - if (options?.Tools is { Count: > 0 }) - { - if (options.ToolMode is RequiredChatToolMode r) - { - toolChoice = r.RequiredFunctionName is null ? new BetaToolChoice(new BetaToolChoiceAny()) : new BetaToolChoice(new BetaToolChoiceTool(r.RequiredFunctionName)); - } - - tools = []; - foreach (var tool in options.Tools) - { - switch (tool) - { - case AIFunctionDeclaration f: - tools.Add(new BetaToolUnion(new BetaTool() - { - Name = f.Name, - Description = f.Description, - InputSchema = AIFunctionDeclarationToInputSchema(f) - })); - break; - - case HostedCodeInterpreterTool codeTool: - if (codeTool.AdditionalProperties?["version"] is string version && version.Contains("20250522")) - { - tools.Add(new BetaCodeExecutionTool20250522()); - } - else - { - tools.Add(new BetaCodeExecutionTool20250825()); - } - break; - - case HostedWebSearchTool webSearchTool: - tools.Add(new BetaToolUnion(new BetaWebSearchTool20250305() - { - MaxUses = (long?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.MaxUses)], - AllowedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.AllowedDomains)], - BlockedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.BlockedDomains)], - CacheControl = (BetaCacheControlEphemeral?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.CacheControl)], - Name = JsonSerializer.Deserialize(JsonSerializer.Serialize(webSearchTool.Name, AnthropicClientJsonContext.Default.String), AnthropicClientJsonContext.Default.JsonElement), - UserLocation = (UserLocation?)webSearchTool.AdditionalProperties?[nameof(UserLocation)] - })); - break; - } - } - } - - MessageCreateParams? providedParameters = options?.RawRepresentationFactory?.Invoke(client) as MessageCreateParams; - - return new MessageCreateParams() - { - Model = modelId, - Messages = GetMessages(messages), - System = GetSystem(options, messages), - MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : providedParameters?.MaxTokens ?? client.DefaultMaxTokens, - Temperature = (options?.Temperature is float temperature) ? (double)temperature : providedParameters?.Temperature, - TopP = (options?.TopP is float topP) ? (double)topP : providedParameters?.TopP, - TopK = (options?.TopK is int topK) ? topK : providedParameters?.TopK, - StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : providedParameters?.StopSequences, - ToolChoice = toolChoice ?? providedParameters?.ToolChoice, - Tools = tools ?? providedParameters?.Tools, - Thinking = options?.GetThinkingParameters() ?? providedParameters?.Thinking, - MCPServers = GetMcpServers(options) ?? providedParameters?.MCPServers, - }; - } - - private static SystemModel? GetSystem(ChatOptions? options, IEnumerable messages) - { - StringBuilder? fullInstructions = (options?.Instructions is string instructions) ? new(instructions) : null; - - foreach (ChatMessage message in messages) - { - if (message.Role == ChatRole.System) - { - (fullInstructions ??= new()).AppendLine(string.Concat(message.Contents.OfType())); - } - } - - return fullInstructions is not null ? new SystemModel(fullInstructions.ToString()) : null; - } - - private static List GetMessages(IEnumerable chatMessages) - { - List betaMessages = []; - - foreach (ChatMessage message in chatMessages) - { - if (message.Role == ChatRole.System) - { - continue; - } - - // Process contents in order, creating new messages when switching between tool results and other content - // This preserves ordering and handles interleaved tool calls, AI output, and tool results - BetaMessageParam? currentMessage = null; - bool lastWasToolResult = false; - - for (var currentIndex = 0; currentIndex < message.Contents.Count; currentIndex++) - { - bool isToolResult = message.Contents[currentIndex] is FunctionResultContent; - - // Create new message if: - // 1. This is the first content item, OR - // 2. We're switching between tool result and non-tool result content - if (currentMessage == null || lastWasToolResult != isToolResult) - { - var messageRole = isToolResult ? Role.User : (message.Role == ChatRole.Assistant ? Role.Assistant : Role.User); - currentMessage = new() - { - // Tool results must always be in User messages, others respect original role - Role = messageRole, - Content = new BetaMessageParamContent(GetContents(message.Contents, currentIndex, messageRole)) - }; - betaMessages.Add(currentMessage); - lastWasToolResult = isToolResult; - } - } - } - - betaMessages.RemoveAll(m => m.Content.TryPickBetaContentBlockParams(out var blocks) && blocks.Count == 0); - - // Avoid errors from completely empty input. - if (betaMessages.Count == 0) - { - betaMessages.Add(new BetaMessageParam() { Role = Role.User, Content = "\u200b" }); // zero-width space - } - - return betaMessages; - } - - private static List GetContents(IList contents, int currentIndex, Role currentRole) - { - bool addedToolResult = false; - List contentBlocks = []; - for (var i = currentIndex; i < contents.Count; i++) - { - switch (contents[i]) - { - case FunctionResultContent frc: - if (addedToolResult) - { - // Any subsequent function result needs to be processed as a new message - goto end; - } - addedToolResult = true; - contentBlocks.Add(new BetaToolResultBlockParam(frc.CallId) - { - Content = new BetaToolResultBlockParamContent(frc.Result?.ToString() ?? string.Empty), - IsError = frc.Exception is not null, - CacheControl = frc.AdditionalProperties?[nameof(BetaToolResultBlockParam.CacheControl)] as BetaCacheControlEphemeral, - ToolUseID = frc.CallId, - }); - break; - - case FunctionCallContent fcc: - contentBlocks.Add(new BetaToolUseBlockParam() - { - ID = fcc.CallId, - Name = fcc.Name, - Input = fcc.Arguments?.ToDictionary(k => k.Key, v => JsonSerializer.SerializeToElement(v.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? new Dictionary() - }); - break; - - case TextReasoningContent reasoningContent: - if (string.IsNullOrEmpty(reasoningContent.Text)) - { - contentBlocks.Add(new BetaRedactedThinkingBlockParam(reasoningContent.ProtectedData!)); - } - else - { - contentBlocks.Add(new BetaThinkingBlockParam() - { - Signature = reasoningContent.ProtectedData!, - Thinking = reasoningContent.Text, - }); - } - break; - - case TextContent textContent: - string text = textContent.Text; - if (currentRole == Role.Assistant) - { - var trimmedText = text.TrimEnd(); - if (!string.IsNullOrEmpty(trimmedText)) - { - contentBlocks.Add(new BetaTextBlockParam() { Text = trimmedText }); - } - } - else if (!string.IsNullOrWhiteSpace(text)) - { - contentBlocks.Add(new BetaTextBlockParam() { Text = text }); - } - - break; - - case HostedFileContent hostedFileContent: - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaFileDocumentSource(hostedFileContent.FileId))))); - break; - - case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): - contentBlocks.Add( - new BetaImageBlockParam( - new BetaImageBlockParamSource( - new BetaBase64ImageSource() - { - Data = Convert.ToBase64String(imageContent.Data.ToArray()), - MediaType = imageContent.MediaType - }))); - break; - - case DataContent pdfDocumentContent when pdfDocumentContent.MediaType == "application/pdf": - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaBase64PDFSource() - { - Data = Convert.ToBase64String(pdfDocumentContent.Data.ToArray()), - })))); - break; - - case DataContent textDocumentContent when textDocumentContent.HasTopLevelMediaType("text"): - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaPlainTextSource() - { - Data = Convert.ToBase64String(textDocumentContent.Data.ToArray()), - })))); - break; - - case UriContent imageUriContent when imageUriContent.HasTopLevelMediaType("image"): - contentBlocks.Add( - new BetaImageBlockParam( - new BetaImageBlockParamSource( - new BetaURLImageSource(imageUriContent.Uri.ToString())))); - break; - - case UriContent pdfUriContent when pdfUriContent.MediaType == "application/pdf": - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaURLPDFSource(pdfUriContent.Uri.ToString()))))); - break; - } - } - -end: - return contentBlocks; - } - - /// - /// Process response content - /// - public static List ProcessResponseContent(BetaMessage response) - { - List contents = new(); - - foreach (BetaContentBlock content in response.Content) - { - switch (content) - { - case BetaContentBlock ct when ct.TryPickThinking(out var thinkingBlock): - contents.Add(new TextReasoningContent(thinkingBlock.Thinking) - { - ProtectedData = thinkingBlock.Signature, - }); - break; - - case BetaContentBlock ct when ct.TryPickRedactedThinking(out var redactedThinkingBlock): - contents.Add(new TextReasoningContent(null) - { - ProtectedData = redactedThinkingBlock.Data, - }); - break; - - case BetaContentBlock ct when ct.TryPickText(out var textBlock): - var textContent = new TextContent(textBlock.Text); - if (textBlock.Citations is { Count: > 0 }) - { - foreach (var tau in textBlock.Citations) - { - var annotation = new CitationAnnotation() - { - RawRepresentation = tau, - Snippet = tau.CitedText, - FileId = tau.Title, - AnnotatedRegions = [] - }; - - switch (tau) - { - case BetaTextCitation bChar when bChar.TryPickCitationCharLocation(out var charLocation): - { - annotation.AnnotatedRegions.Add(new TextSpanAnnotatedRegion { StartIndex = (int?)charLocation?.StartCharIndex, EndIndex = (int?)charLocation?.EndCharIndex }); - break; - } - - case BetaTextCitation search when search.TryPickCitationSearchResultLocation(out var searchLocation) && Uri.IsWellFormedUriString(searchLocation.Source, UriKind.RelativeOrAbsolute): - { - annotation.Url = new Uri(searchLocation.Source); - break; - } - - case BetaTextCitation search when search.TryPickCitationsWebSearchResultLocation(out var searchLocation): - { - annotation.Url = new Uri(searchLocation.URL); - break; - } - - default: - { - (textContent.Annotations ?? []).Add(new CitationAnnotation - { - Snippet = tau.CitedText, - Title = tau.Title, - RawRepresentation = tau - }); - break; - } - } - - (textContent.Annotations ??= []).Add(annotation); - } - } - contents.Add(textContent); - break; - - case BetaContentBlock ct when ct.TryPickToolUse(out var toolUse): - contents.Add(new FunctionCallContent(toolUse.ID, toolUse.Name) - { - Arguments = toolUse.Input?.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), - RawRepresentation = toolUse - }); - break; - - case BetaContentBlock ct when ct.TryPickMCPToolUse(out var mcpToolUse): -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - contents.Add(new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) - { - Arguments = mcpToolUse.Input.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), - RawRepresentation = mcpToolUse - }); - break; - - case BetaContentBlock ct when ct.TryPickMCPToolResult(out var mcpToolResult): - { - contents.Add(new McpServerToolResultContent(mcpToolResult.ToolUseID) - { - Output = [mcpToolResult.IsError - ? new ErrorContent(mcpToolResult.Content.Value.ToString()) - : new TextContent(mcpToolResult.Content.Value.ToString())], - RawRepresentation = mcpToolResult - }); - break; - } - - case BetaContentBlock ct when ct.TryPickCodeExecutionToolResult(out var cer): - { - var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; - if (cer.Content.TryPickError(out var cerErr)) - { - codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = cerErr.ErrorCode.Value().ToString() }); - } - if (cer.Content.TryPickResultBlock(out var cerResult)) - { - codeResult.Outputs.Add(new TextContent(cerResult.Stdout) { RawRepresentation = cerResult }); - if (!string.IsNullOrWhiteSpace(cerResult.Stderr)) - { - codeResult.Outputs.Add(new TextContent(cerResult.Stderr) { RawRepresentation = cerResult }); - } - } - - contents.Add(codeResult); - break; - } - - case BetaContentBlock ct when ct.TryPickBashCodeExecutionToolResult(out var bashCer): - { - var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; - if (bashCer.Content.TryPickBetaBashCodeExecutionToolResultError(out var bashCerErr)) - { - codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = bashCerErr.ErrorCode.Value().ToString() }); - } - if (bashCer.Content.TryPickBetaBashCodeExecutionResultBlock(out var bashCerResult)) - { - codeResult.Outputs.Add(new TextContent(bashCerResult.Stdout) { RawRepresentation = bashCerResult }); - if (!string.IsNullOrWhiteSpace(bashCerResult.Stderr)) - { - codeResult.Outputs.Add(new TextContent(bashCerResult.Stderr) { RawRepresentation = bashCerResult }); - } - } - - contents.Add(codeResult); - break; - } -#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - - default: - { - contents.Add(new AIContent { RawRepresentation = content }); - break; - } - } - } - - return contents; - } - - /// - /// Function parameters class - /// - private sealed class FunctionParameters - { - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public List Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; - } -} From d44f62ce4e077999cce7baef3f7a2ba19f90aeb6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:47:00 +0000 Subject: [PATCH 06/24] Update Anthropic Impl --- dotnet/Directory.Packages.props | 3 +- dotnet/agent-framework-dotnet.slnx | 3 - .../Program.cs | 8 + .../Agent_Anthropic_Step01_Running/Program.cs | 1 - .../Program.cs | 3 - .../Program.cs | 2 - .../AnthropicBetaChatClient.cs | 9 + .../AnthropicClientExtensions.cs | 75 +- .../External/AnthropicBetaClientExtensions.cs | 937 ++++++++++++++++++ .../External/AnthropicClientExtensions.cs | 736 ++++++++++++++ .../Microsoft.Agents.AI.Anthropic.csproj | 11 +- 11 files changed, 1733 insertions(+), 55 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 80ae322d07..c34c9988cc 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,7 +11,8 @@ - + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 70cb9a9f66..e01cb30a36 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -391,7 +391,4 @@ - - - diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs index 9b03c989e1..05caf37c62 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs @@ -3,7 +3,9 @@ // 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; +using OpenAI.Responses; var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set."); var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; @@ -15,3 +17,9 @@ // Invoke the agent and output the text result. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); + +var responseCreationOptions = new ResponseCreationOptions(); +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +responseCreationOptions.Patch.Set("$.prompt_cache_key"u8, BinaryData.FromString("custom_key")); +responseCreationOptions.Patch.Set("$.prompt_cache_retention"u8, BinaryData.FromString("24h")); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs index 8dea9a81bf..27242c2701 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs @@ -3,7 +3,6 @@ // This sample shows how to create and use a simple AI agent with OpenAI as the backend. using Anthropic; -using Anthropic.Client; using Anthropic.Core; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs index cd1bbaab12..2532aa606c 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -3,7 +3,6 @@ // This sample shows how to create and use an AI agent with reasoning capabilities. using Anthropic; -using Anthropic.Client; using Anthropic.Core; using Anthropic.Models.Beta.Messages; using Microsoft.Agents.AI; @@ -34,7 +33,6 @@ 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.")) { @@ -50,4 +48,3 @@ } } } -*/ diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs index 6e542214f4..a56db8d4a2 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/Program.cs @@ -30,10 +30,8 @@ static string GetWeather([Description("The location to get the weather for.")] s 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/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs index 1ad9e14340..8a600ce1ba 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs @@ -539,6 +539,14 @@ private static List ProcessResponseContent(BetaMessage response) { codeResult.Outputs.Add(new TextContent(cerResult.Stderr) { RawRepresentation = cerResult }); } + + if (cerResult.Content is { Count: > 0 }) + { + foreach (var ceOutputContent in cerResult.Content) + { + codeResult.Outputs.Add(new HostedFileContent(ceOutputContent.FileID)); + } + } } contents.Add(codeResult); @@ -580,4 +588,5 @@ private static List ProcessResponseContent(BetaMessage response) [JsonSerializable(typeof(JsonElement))] [JsonSerializable(typeof(string))] +[JsonSerializable(typeof(Dictionary))] internal sealed partial class AnthropicClientJsonContext : JsonSerializerContext; diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index 1dfdbda0ad..61c92a2f6d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. -using Anthropic.Models.Beta.Messages; -using Microsoft.Agents.AI; -using Microsoft.Agents.AI.Anthropic; +using Anthropic; +using Anthropic.Services; using Microsoft.Extensions.AI; -namespace Anthropic; +namespace Microsoft.Agents.AI; /// -/// Provides extension methods for the class. +/// Provides extension methods for the class. /// public static class AnthropicClientExtensions { @@ -29,13 +28,13 @@ public static class AnthropicClientExtensions /// The default maximum tokens for chat completions. Defaults to if not provided. /// The created AI agent. public static ChatClientAgent CreateAIAgent( - this AnthropicClient client, + this IAnthropicClient client, string model, - string? instructions, + string? instructions = null, string? name = null, string? description = null, IList? tools = null, - long? defaultMaxTokens = null) + int? defaultMaxTokens = null) { var options = new ChatClientAgentOptions { @@ -53,41 +52,37 @@ public static ChatClientAgent CreateAIAgent( } /// - /// Get an compatible implementation around the . + /// Creates a new AI agent using the specified model and options. /// - /// The Anthropic client. - /// The default model to use for chat completions. + /// 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. - /// The implementation. - public static IChatClient AsIChatClient( - this AnthropicClient client, - string defaultModelId, - long? defaultMaxTokens = null) + /// 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) { - return new AnthropicBetaChatClient(client, defaultMaxTokens ?? DefaultMaxTokens, defaultModelId: defaultModelId); - } + var options = new ChatClientAgentOptions + { + Instructions = instructions, + Name = name, + Description = description, + }; - /// Creates an to represent a raw . - /// The tool to wrap as an . - /// The wrapped as an . - /// - /// - /// The returned tool is only suitable for use with the returned by - /// (or s that delegate - /// to such an instance). It is likely to be ignored by any other implementation. - /// - /// - /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, - /// such as , , , or - /// , those types should be preferred instead of this method, as they are more portable, - /// capable of being respected by any implementation. This method does not attempt to - /// map the supplied to any of those types, it simply wraps it as-is: - /// the returned by will - /// be able to unwrap the when it processes the list of tools. - /// - /// - public static AITool AsAITool(this BetaTool tool) - { - return new AnthropicBetaChatClient.BetaToolAITool(tool); + if (tools is { Count: > 0 }) + { + options.ChatOptions = new ChatOptions { Tools = tools }; + } + + return new ChatClientAgent(betaService.AsIChatClient(model, defaultMaxTokens), options); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs new file mode 100644 index 0000000000..5cd3521ca8 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs @@ -0,0 +1,937 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Anthropic; +using Anthropic.Core; +using Anthropic.Models.Beta.Messages; +using Anthropic.Services; +using Microsoft.Agents.AI.Anthropic; + +#pragma warning disable MEAI001 // [Experimental] APIs in Microsoft.Extensions.AI +#pragma warning disable IDE0130 // Namespace does not match folder structure + +namespace Microsoft.Extensions.AI; + +/// +/// Provides extension methods for working with Anthropic beta services, including conversion to chat clients and tool +/// wrapping for use with Anthropic-specific chat implementations. +/// +public static class AnthropicBetaClientExtensions +{ + /// Gets an for use with this . + /// The beta service. + /// + /// The default ID of the model to use. + /// If , it must be provided per request via . + /// + /// + /// The default maximum number of tokens to generate in a response. + /// This may be overridden with . + /// If no value is provided for this parameter or in , a default maximum will be used. + /// + /// An that can be used to converse via the . + /// is . + public static IChatClient AsIChatClient( + this IBetaService betaService, + string? defaultModelId = null, + int? defaultMaxOutputTokens = null) + { + if (betaService is null) + { + throw new ArgumentNullException(nameof(betaService)); + } + + if (defaultMaxOutputTokens is <= 0) + { + throw new ArgumentOutOfRangeException(nameof(defaultMaxOutputTokens), "Default max tokens must be greater than zero."); + } + + return new AnthropicChatClient(betaService, defaultModelId, defaultMaxOutputTokens); + } + + /// Creates an to represent a raw . + /// The tool to wrap as an . + /// The wrapped as an . + /// + /// + /// The returned tool is only suitable for use with the returned by + /// (or s that delegate + /// to such an instance). It is likely to be ignored by any other implementation. + /// + /// + /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, + /// such as , , , or + /// , those types should be preferred instead of this method, as they are more portable, + /// capable of being respected by any implementation. This method does not attempt to + /// map the supplied to any of those types, it simply wraps it as-is: + /// the returned by will + /// be able to unwrap the when it processes the list of tools. + /// + /// + public static AITool AsAITool(this BetaToolUnion tool) + { + if (tool is null) + { + throw new ArgumentNullException(nameof(tool)); + } + + return new ToolUnionAITool(tool); + } + + private sealed class AnthropicChatClient( + IBetaService betaService, + string? defaultModelId, + int? defaultMaxOutputTokens) : IChatClient + { + private const int DefaultMaxTokens = 1024; + + private static readonly AIJsonSchemaTransformCache s_transformCache = new(new AIJsonSchemaTransformOptions + { + DisallowAdditionalProperties = true, + TransformSchemaNode = (ctx, schemaNode) => + { + if (schemaNode is JsonObject schemaObj) + { + // From https://platform.claude.com/docs/en/build-with-claude/structured-outputs + ReadOnlySpan unsupportedProperties = + [ + "minimum", "maximum", "multipleOf", + "minLength", "maxLength", + ]; + + foreach (string propName in unsupportedProperties) + { + _ = schemaObj.Remove(propName); + } + } + + return schemaNode; + }, + }); + + private readonly IBetaService _betaService = betaService; + private readonly string? _defaultModelId = defaultModelId; + private readonly int _defaultMaxTokens = defaultMaxOutputTokens ?? DefaultMaxTokens; + private readonly IAnthropicClient _anthropicClient = typeof(BetaService) + .GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! + .GetValue(betaService) as IAnthropicClient ?? throw new InvalidOperationException("Unable to access Anthropic client from BetaService."); + + private ChatClientMetadata? _metadata; + + /// + void IDisposable.Dispose() { } + + /// + public object? GetService(System.Type serviceType, object? serviceKey = null) + { + if (serviceType is null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (serviceKey is not null) + { + return null; + } + + if (serviceType == typeof(ChatClientMetadata)) + { + return this._metadata ??= new("anthropic", this._anthropicClient.BaseUrl, this._defaultModelId); + } + + if (serviceType.IsInstanceOfType(this._betaService)) + { + return this._betaService; + } + + if (serviceType.IsInstanceOfType(this)) + { + return this; + } + + return null; + } + + /// + public async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + if (messages is null) + { + throw new ArgumentNullException(nameof(messages)); + } + + List messageParams = CreateMessageParams(messages, out List? systemMessages); + MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); + + var createResult = await this._betaService.Messages.Create(createParams, cancellationToken).ConfigureAwait(false); + + ChatMessage m = new(ChatRole.Assistant, [.. createResult.Content.Select(ToAIContent)]) + { + CreatedAt = DateTimeOffset.UtcNow, + MessageId = createResult.ID, + }; + + return new(m) + { + CreatedAt = m.CreatedAt, + FinishReason = ToFinishReason(createResult.StopReason), + ModelId = createResult.Model.Raw() ?? createParams.Model.Raw(), + RawRepresentation = createResult, + ResponseId = m.MessageId, + Usage = createResult.Usage is { } usage ? ToUsageDetails(usage) : null, + }; + } + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (messages is null) + { + throw new ArgumentNullException(nameof(messages)); + } + + List messageParams = CreateMessageParams(messages, out List? systemMessages); + MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); + + string? messageId = null; + string? modelID = null; + UsageDetails? usageDetails = null; + ChatFinishReason? finishReason = null; + Dictionary? streamingFunctions = null; + + await foreach (var createResult in this._betaService.Messages.CreateStreaming(createParams, cancellationToken).WithCancellation(cancellationToken)) + { + List contents = []; + + switch (createResult.Value) + { + case BetaRawMessageStartEvent rawMessageStart: + if (string.IsNullOrWhiteSpace(messageId)) + { + messageId = rawMessageStart.Message.ID; + } + + if (string.IsNullOrWhiteSpace(modelID)) + { + modelID = rawMessageStart.Message.Model; + } + + if (rawMessageStart.Message.Usage is { } usage) + { + UsageDetails current = ToUsageDetails(usage); + if (usageDetails is null) + { + usageDetails = current; + } + else + { + usageDetails.Add(current); + } + } + break; + + case BetaRawMessageDeltaEvent rawMessageDelta: + finishReason = ToFinishReason(rawMessageDelta.Delta.StopReason); + if (rawMessageDelta.Usage is { } deltaUsage) + { + UsageDetails current = ToUsageDetails(deltaUsage); + if (usageDetails is null) + { + usageDetails = current; + } + else + { + usageDetails.Add(current); + } + } + break; + + case BetaRawContentBlockStartEvent contentBlockStart: + switch (contentBlockStart.ContentBlock.Value) + { + case BetaTextBlock text: + contents.Add(new TextContent(text.Text) + { + RawRepresentation = text, + }); + break; + + case BetaThinkingBlock thinking: + contents.Add(new TextReasoningContent(thinking.Thinking) + { + ProtectedData = thinking.Signature, + RawRepresentation = thinking, + }); + break; + + case BetaRedactedThinkingBlock redactedThinking: + contents.Add(new TextReasoningContent(string.Empty) + { + ProtectedData = redactedThinking.Data, + RawRepresentation = redactedThinking, + }); + break; + + case BetaToolUseBlock toolUse: + streamingFunctions ??= []; + streamingFunctions[contentBlockStart.Index] = new() + { + CallId = toolUse.ID, + Name = toolUse.Name, + }; + break; + } + break; + + case BetaRawContentBlockDeltaEvent contentBlockDelta: + switch (contentBlockDelta.Delta.Value) + { + case BetaTextDelta textDelta: + contents.Add(new TextContent(textDelta.Text) + { + RawRepresentation = textDelta, + }); + break; + + case BetaInputJSONDelta inputDelta: + if (streamingFunctions is not null && + streamingFunctions.TryGetValue(contentBlockDelta.Index, out var functionData)) + { + functionData.Arguments.Append(inputDelta.PartialJSON); + } + break; + + case BetaThinkingDelta thinkingDelta: + contents.Add(new TextReasoningContent(thinkingDelta.Thinking) + { + RawRepresentation = thinkingDelta, + }); + break; + + case BetaSignatureDelta signatureDelta: + contents.Add(new TextReasoningContent(null) + { + ProtectedData = signatureDelta.Signature, + RawRepresentation = signatureDelta, + }); + break; + } + break; + + case BetaRawContentBlockStopEvent contentBlockStop: + if (streamingFunctions is not null) + { + foreach (var sf in streamingFunctions) + { + contents.Add(FunctionCallContent.CreateFromParsedArguments(sf.Value.Arguments.ToString(), sf.Value.CallId, sf.Value.Name, + json => JsonSerializer.Deserialize>(json, AnthropicClientJsonContext.Default.DictionaryStringObject))); + } + } + break; + } + + yield return new(ChatRole.Assistant, contents) + { + CreatedAt = DateTimeOffset.UtcNow, + FinishReason = finishReason, + MessageId = messageId, + RawRepresentation = createResult, + ResponseId = messageId, + ModelId = modelID, + }; + } + + if (usageDetails is not null) + { + yield return new(ChatRole.Assistant, [new UsageContent(usageDetails)]) + { + CreatedAt = DateTimeOffset.UtcNow, + FinishReason = finishReason, + MessageId = messageId, + ResponseId = messageId, + ModelId = modelID, + }; + } + } + + private static List CreateMessageParams(IEnumerable messages, out List? systemMessages) + { + List messageParams = []; + systemMessages = null; + + foreach (ChatMessage message in messages) + { + if (message.Role == ChatRole.System) + { + foreach (AIContent content in message.Contents) + { + if (content is TextContent tc) + { + (systemMessages ??= []).Add(new() { Text = tc.Text }); + } + } + + continue; + } + + List contents = []; + + foreach (AIContent content in message.Contents) + { + switch (content) + { + case AIContent ac when ac.RawRepresentation is BetaContentBlockParam rawContent: + contents.Add(rawContent); + break; + + case TextContent tc: + string text = tc.Text; + if (message.Role == ChatRole.Assistant) + { + text = text.TrimEnd(); + if (!string.IsNullOrWhiteSpace(text)) + { + contents.Add(new BetaTextBlockParam() { Text = text }); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + contents.Add(new BetaTextBlockParam() { Text = text }); + } + break; + + case TextReasoningContent trc when !string.IsNullOrEmpty(trc.Text): + contents.Add(new BetaThinkingBlockParam() + { + Thinking = trc.Text, + Signature = trc.ProtectedData ?? string.Empty, + }); + break; + + case TextReasoningContent trc when !string.IsNullOrEmpty(trc.ProtectedData): + contents.Add(new BetaRedactedThinkingBlockParam() + { + Data = trc.ProtectedData!, + }); + break; + + case DataContent dc when dc.HasTopLevelMediaType("image"): + contents.Add(new BetaImageBlockParam() + { + Source = new(new BetaBase64ImageSource() { Data = dc.Base64Data.ToString(), MediaType = dc.MediaType }) + }); + break; + + case DataContent dc when string.Equals(dc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): + contents.Add(new BetaRequestDocumentBlock() + { + Source = new(new BetaBase64PDFSource() { Data = dc.Base64Data.ToString() }), + }); + break; + + case DataContent dc when dc.HasTopLevelMediaType("text"): + contents.Add(new BetaRequestDocumentBlock() + { + Source = new(new BetaPlainTextSource() { Data = Encoding.UTF8.GetString(dc.Data.ToArray()) }), + }); + break; + + case UriContent uc when uc.HasTopLevelMediaType("image"): + contents.Add(new BetaImageBlockParam() + { + Source = new(new BetaURLImageSource() { URL = uc.Uri.AbsoluteUri }), + }); + break; + + case UriContent uc when string.Equals(uc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): + contents.Add(new BetaRequestDocumentBlock() + { + Source = new(new BetaURLPDFSource() { URL = uc.Uri.AbsoluteUri }), + }); + break; + + case HostedFileContent fc: + contents.Add(new BetaRequestDocumentBlock() + { + Source = new(new BetaFileDocumentSource(fc.FileId)) + }); + break; + + case FunctionCallContent fcc: + contents.Add(new BetaToolUseBlockParam() + { + ID = fcc.CallId, + Name = fcc.Name, + Input = fcc.Arguments?.ToDictionary(e => e.Key, e => e.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(e.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? [], + }); + break; + + case FunctionResultContent frc: + contents.Add(new BetaToolResultBlockParam() + { + ToolUseID = frc.CallId, + IsError = frc.Exception is not null, + Content = new(JsonSerializer.Serialize(frc.Result, AnthropicClientJsonContext.Default.JsonElement)), + }); + break; + } + } + + if (contents.Count == 0) + { + continue; + } + + messageParams.Add(new() + { + Role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User, + Content = contents, + }); + } + + if (messageParams.Count == 0) + { + messageParams.Add(new() { Role = Role.User, Content = new("\u200b") }); // zero-width space + } + + return messageParams; + } + + private MessageCreateParams GetMessageCreateParams(List messages, List? systemMessages, ChatOptions? options) + { + // Get the initial MessageCreateParams, either with a raw representation provided by the options + // or with only the required properties set. + MessageCreateParams? createParams = options?.RawRepresentationFactory?.Invoke(this) as MessageCreateParams; + if (createParams is not null) + { + // Merge any messages preconfigured on the params with the ones provided to the IChatClient. + createParams = createParams with { Messages = [.. createParams.Messages, .. messages] }; + } + else + { + createParams = new MessageCreateParams() + { + MaxTokens = options?.MaxOutputTokens ?? this._defaultMaxTokens, + Messages = messages, + Model = options?.ModelId ?? this._defaultModelId ?? throw new InvalidOperationException("Model ID must be specified either in ChatOptions or as the default for the client."), + }; + } + + if (options is not null) + { + if (options.Instructions is { } instructions) + { + (systemMessages ??= []).Add(new BetaTextBlockParam() { Text = instructions }); + } + + if (createParams.OutputFormat is null && options.ResponseFormat is { } responseFormat) + { + switch (responseFormat) + { + case ChatResponseFormatJson formatJson when formatJson.Schema is not null: + JsonElement schema = s_transformCache.GetOrCreateTransformedSchema(formatJson).GetValueOrDefault(); + if (schema.TryGetProperty("properties", out JsonElement properties) && properties.ValueKind is JsonValueKind.Object && + schema.TryGetProperty("required", out JsonElement required) && required.ValueKind is JsonValueKind.Array) + { + createParams = createParams with + { + OutputFormat = new BetaJSONOutputFormat() + { + Schema = new() + { + ["type"] = JsonElement.Parse("\"object\""), + ["properties"] = properties, + ["required"] = required, + ["additionalProperties"] = JsonElement.Parse("false"), + }, + }, + }; + } + break; + } + } + + if (options.StopSequences is { Count: > 0 } stopSequences) + { + createParams = createParams.StopSequences is { } existingSequences ? + createParams with { StopSequences = [.. existingSequences, .. stopSequences] } : + createParams with { StopSequences = [.. stopSequences] }; + } + + if (createParams.Temperature is null && options.Temperature is { } temperature) + { + createParams = createParams with { Temperature = temperature }; + } + + if (createParams.TopK is null && options.TopK is { } topK) + { + createParams = createParams with { TopK = topK }; + } + + if (createParams.TopP is null && options.TopP is { } topP) + { + createParams = createParams with { TopP = topP }; + } + + if (options.Tools is { } tools) + { + List? createdTools = createParams.Tools; + List? mcpServers = createParams.MCPServers; + foreach (var tool in tools) + { + switch (tool) + { + case ToolUnionAITool raw: + (createdTools ??= []).Add(raw.Tool); + break; + + case AIFunctionDeclaration af: + Dictionary properties = []; + List required = []; + JsonElement inputSchema = af.JsonSchema; + if (inputSchema.ValueKind is JsonValueKind.Object) + { + if (inputSchema.TryGetProperty("properties", out JsonElement propsElement) && propsElement.ValueKind is JsonValueKind.Object) + { + foreach (JsonProperty p in propsElement.EnumerateObject()) + { + properties[p.Name] = p.Value; + } + } + + if (inputSchema.TryGetProperty("required", out JsonElement reqElement) && reqElement.ValueKind is JsonValueKind.Array) + { + foreach (JsonElement r in reqElement.EnumerateArray()) + { + if (r.ValueKind is JsonValueKind.String && r.GetString() is { } s && !string.IsNullOrWhiteSpace(s)) + { + required.Add(s); + } + } + } + } + + (createdTools ??= []).Add(new BetaTool() + { + Name = af.Name, + Description = af.Description, + InputSchema = new(properties) { Required = required }, + }); + break; + + case HostedWebSearchTool: + (createdTools ??= []).Add(new BetaWebSearchTool20250305()); + break; + + case HostedCodeInterpreterTool: + (createdTools ??= []).Add(new BetaCodeExecutionTool20250825()); + break; + + case HostedMcpServerTool mcp: + (mcpServers ??= []).Add(mcp.AllowedTools is { Count: > 0 } allowedTools ? + new() + { + Name = mcp.Name, + URL = mcp.ServerAddress, + ToolConfiguration = new() + { + AllowedTools = [.. allowedTools], + Enabled = true, + } + } : + new() + { + Name = mcp.Name, + URL = mcp.ServerAddress, + }); + break; + } + } + + if (createdTools?.Count > 0) + { + createParams = createParams with { Tools = createdTools }; + } + + if (mcpServers?.Count > 0) + { + createParams = createParams with { MCPServers = mcpServers }; + } + } + + if (createParams.ToolChoice is null && options.ToolMode is { } toolMode) + { + BetaToolChoice? toolChoice = + toolMode is AutoChatToolMode ? new BetaToolChoiceAuto() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : + toolMode is NoneChatToolMode ? new BetaToolChoiceNone() : + toolMode is RequiredChatToolMode ? new BetaToolChoiceAny() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : + (BetaToolChoice?)null; + if (toolChoice is not null) + { + createParams = createParams with { ToolChoice = toolChoice }; + } + } + } + + if (systemMessages is not null) + { + if (createParams.System is { } existingSystem) + { + if (existingSystem.Value is string existingMessage) + { + systemMessages.Insert(0, new BetaTextBlockParam() { Text = existingMessage }); + } + else if (existingSystem.Value is IReadOnlyList existingMessages) + { + systemMessages.InsertRange(0, existingMessages); + } + } + + createParams = createParams with { System = systemMessages }; + } + + return createParams; + } + + private static UsageDetails ToUsageDetails(BetaUsage usage) => + ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); + + private static UsageDetails ToUsageDetails(BetaMessageDeltaUsage usage) => + ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); + + private static UsageDetails ToUsageDetails(long? inputTokens, long? outputTokens, long? cacheCreationInputTokens, long? cacheReadInputTokens) + { + UsageDetails usageDetails = new() + { + InputTokenCount = inputTokens, + OutputTokenCount = outputTokens, + TotalTokenCount = (inputTokens is not null || outputTokens is not null) ? (inputTokens ?? 0) + (outputTokens ?? 0) : null, + }; + + if (cacheCreationInputTokens is not null) + { + (usageDetails.AdditionalCounts ??= [])[nameof(BetaUsage.CacheCreationInputTokens)] = cacheCreationInputTokens.Value; + } + + if (cacheReadInputTokens is not null) + { + (usageDetails.AdditionalCounts ??= [])[nameof(BetaUsage.CacheReadInputTokens)] = cacheReadInputTokens.Value; + } + + return usageDetails; + } + + private static ChatFinishReason? ToFinishReason(ApiEnum? stopReason) => + stopReason?.Value() switch + { + null => null, + BetaStopReason.Refusal => ChatFinishReason.ContentFilter, + BetaStopReason.MaxTokens => ChatFinishReason.Length, + BetaStopReason.ToolUse => ChatFinishReason.ToolCalls, + _ => ChatFinishReason.Stop, + }; + + private static AIContent ToAIContent(BetaContentBlock block) + { + static AIContent FromBetaTextBlock(BetaTextBlock text) + { + TextContent tc = new(text.Text) + { + RawRepresentation = text, + }; + + if (text.Citations is { Count: > 0 }) + { + tc.Annotations = [.. text.Citations.Select(ToAIAnnotation).OfType()]; + } + + return tc; + } + + switch (block.Value) + { + case BetaTextBlock text: + return FromBetaTextBlock(text); + + case BetaThinkingBlock thinking: + return new TextReasoningContent(thinking.Thinking) + { + ProtectedData = thinking.Signature, + RawRepresentation = thinking, + }; + + case BetaRedactedThinkingBlock redactedThinking: + return new TextReasoningContent(string.Empty) + { + ProtectedData = redactedThinking.Data, + RawRepresentation = redactedThinking, + }; + + case BetaToolUseBlock toolUse: + return new FunctionCallContent( + toolUse.ID, + toolUse.Name, + toolUse.Properties.TryGetValue("input", out JsonElement element) ? + JsonSerializer.Deserialize>(element, AnthropicClientJsonContext.Default.DictionaryStringObject) : + null) + { + RawRepresentation = toolUse, + }; + + case BetaMCPToolUseBlock mcpToolUse: + return new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) + { + Arguments = mcpToolUse.Input.ToDictionary(e => e.Key, e => (object?)e.Value), + RawRepresentation = mcpToolUse, + }; + + case BetaMCPToolResultBlock mcpToolResult: + return new McpServerToolResultContent(mcpToolResult.ToolUseID) + { + Output = + mcpToolResult.IsError ? [new ErrorContent(mcpToolResult.Content.Value?.ToString())] : + mcpToolResult.Content.Value switch + { + string s => [new TextContent(s)], + IReadOnlyList texts => texts.Select(FromBetaTextBlock).ToList(), + _ => null, + }, + RawRepresentation = mcpToolResult, + }; + + case BetaCodeExecutionToolResultBlock ce: + { + CodeInterpreterToolResultContent c = new() + { + CallId = ce.ToolUseID, + RawRepresentation = ce, + }; + + if (ce.Content.TryPickError(out var ceError)) + { + (c.Outputs ??= []).Add(new ErrorContent(null) { ErrorCode = ceError.ErrorCode.Value().ToString() }); + } + + if (ce.Content.TryPickResultBlock(out var ceOutput)) + { + if (!string.IsNullOrWhiteSpace(ceOutput.Stdout)) + { + (c.Outputs ??= []).Add(new TextContent(ceOutput.Stdout)); + } + + if (!string.IsNullOrWhiteSpace(ceOutput.Stderr) || ceOutput.ReturnCode != 0) + { + (c.Outputs ??= []).Add(new ErrorContent(ceOutput.Stderr) + { + ErrorCode = ceOutput.ReturnCode.ToString(CultureInfo.InvariantCulture) + }); + } + + if (ceOutput.Content is { Count: > 0 }) + { + foreach (var ceOutputContent in ceOutput.Content) + { + (c.Outputs ??= []).Add(new HostedFileContent(ceOutputContent.FileID)); + } + } + } + + return c; + } + + case BetaBashCodeExecutionToolResultBlock ce: + // This is the same as BetaCodeExecutionToolResultBlock but with a different type names. + // Keep both of them in sync. + { + CodeInterpreterToolResultContent c = new() + { + CallId = ce.ToolUseID, + RawRepresentation = ce, + }; + + if (ce.Content.TryPickBetaBashCodeExecutionToolResultError(out var ceError)) + { + (c.Outputs ??= []).Add(new ErrorContent(null) { ErrorCode = ceError.ErrorCode.Value().ToString() }); + } + + if (ce.Content.TryPickBetaBashCodeExecutionResultBlock(out var ceOutput)) + { + if (!string.IsNullOrWhiteSpace(ceOutput.Stdout)) + { + (c.Outputs ??= []).Add(new TextContent(ceOutput.Stdout)); + } + + if (!string.IsNullOrWhiteSpace(ceOutput.Stderr) || ceOutput.ReturnCode != 0) + { + (c.Outputs ??= []).Add(new ErrorContent(ceOutput.Stderr) + { + ErrorCode = ceOutput.ReturnCode.ToString(CultureInfo.InvariantCulture) + }); + } + + if (ceOutput.Content is { Count: > 0 }) + { + foreach (var ceOutputContent in ceOutput.Content) + { + (c.Outputs ??= []).Add(new HostedFileContent(ceOutputContent.FileID)); + } + } + } + + return c; + } + + default: + return new AIContent() + { + RawRepresentation = block.Value + }; + } + } + + private static AIAnnotation? ToAIAnnotation(BetaTextCitation citation) + { + CitationAnnotation annotation = new() + { + Title = citation.Title ?? citation.DocumentTitle, + Snippet = citation.CitedText, + FileId = citation.FileID, + }; + + if (citation.TryPickCitationsWebSearchResultLocation(out var webSearchLocation)) + { + annotation.Url = Uri.TryCreate(webSearchLocation.URL, UriKind.Absolute, out Uri? url) ? url : null; + } + else if (citation.TryPickCitationSearchResultLocation(out var searchLocation)) + { + annotation.Url = Uri.TryCreate(searchLocation.Source, UriKind.Absolute, out Uri? url) ? url : null; + } + + return annotation; + } + + private sealed class StreamingFunctionData + { + public string CallId { get; set; } = ""; + public string Name { get; set; } = ""; + public StringBuilder Arguments { get; } = new(); + } + } + + private sealed class ToolUnionAITool(BetaToolUnion tool) : AITool + { + public BetaToolUnion Tool => tool; + + public override string Name => tool.Value?.GetType().Name ?? base.Name; + + public override object? GetService(System.Type serviceType, object? serviceKey = null) => + serviceKey is null && serviceType?.IsInstanceOfType(tool) is true ? tool : + base.GetService(serviceType!, serviceKey); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs new file mode 100644 index 0000000000..15eac3ce06 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs @@ -0,0 +1,736 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Pollyfil from + +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using Anthropic; +using Anthropic.Core; +using Anthropic.Models.Messages; +using Microsoft.Agents.AI.Anthropic; + +#pragma warning disable IDE0130 // Namespace does not match folder structure + +namespace Microsoft.Extensions.AI; + +/// +/// Provides extension methods for Anthropic clients with chat interfaces and tool representations. +/// +public static class AnthropicClientExtensions +{ + /// Gets an for use with this . + /// The client. + /// + /// The default ID of the model to use. + /// If , it must be provided per request via . + /// + /// + /// The default maximum number of tokens to generate in a response. + /// This may be overridden with . + /// If no value is provided for this parameter or in , a default maximum will be used. + /// + /// An that can be used to converse via the . + /// is . + public static IChatClient AsIChatClient( + this IAnthropicClient client, + string? defaultModelId = null, + int? defaultMaxOutputTokens = null) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (defaultMaxOutputTokens is <= 0) + { + throw new ArgumentOutOfRangeException(nameof(defaultMaxOutputTokens), "Default max tokens must be greater than zero."); + } + + return new AnthropicChatClient(client, defaultModelId, defaultMaxOutputTokens); + } + + /// Creates an to represent a raw . + /// The tool to wrap as an . + /// The wrapped as an . + /// + /// + /// The returned tool is only suitable for use with the returned by + /// (or s that delegate + /// to such an instance). It is likely to be ignored by any other implementation. + /// + /// + /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, + /// such as , , , or + /// , those types should be preferred instead of this method, as they are more portable, + /// capable of being respected by any implementation. This method does not attempt to + /// map the supplied to any of those types, it simply wraps it as-is: + /// the returned by will + /// be able to unwrap the when it processes the list of tools. + /// + /// + public static AITool AsAITool(this ToolUnion tool) + { + if (tool is null) + { + throw new ArgumentNullException(nameof(tool)); + } + + return new ToolUnionAITool(tool); + } + + private sealed class AnthropicChatClient( + IAnthropicClient anthropicClient, + string? defaultModelId, + int? defaultMaxTokens) : IChatClient + { + private const int DefaultMaxTokens = 1024; + + private readonly IAnthropicClient _anthropicClient = anthropicClient; + private readonly string? _defaultModelId = defaultModelId; + private readonly int _defaultMaxTokens = defaultMaxTokens ?? DefaultMaxTokens; + private ChatClientMetadata? _metadata; + + /// + void IDisposable.Dispose() { } + + /// + public object? GetService(System.Type serviceType, object? serviceKey = null) + { + if (serviceType is null) + { + throw new ArgumentNullException(nameof(serviceType)); + } + + if (serviceKey is not null) + { + return null; + } + + if (serviceType == typeof(ChatClientMetadata)) + { + return this._metadata ??= new("anthropic", this._anthropicClient.BaseUrl, this._defaultModelId); + } + + if (serviceType.IsInstanceOfType(this._anthropicClient)) + { + return this._anthropicClient; + } + + if (serviceType.IsInstanceOfType(this)) + { + return this; + } + + return null; + } + + /// + public async Task GetResponseAsync( + IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + if (messages is null) + { + throw new ArgumentNullException(nameof(messages)); + } + + List messageParams = CreateMessageParams(messages, out List? systemMessages); + MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); + + var createResult = await this._anthropicClient.Messages.Create(createParams, cancellationToken).ConfigureAwait(false); + + ChatMessage m = new(ChatRole.Assistant, [.. createResult.Content.Select(ToAIContent)]) + { + CreatedAt = DateTimeOffset.UtcNow, + MessageId = createResult.ID, + }; + + return new(m) + { + CreatedAt = m.CreatedAt, + FinishReason = ToFinishReason(createResult.StopReason), + ModelId = createResult.Model.Raw() ?? createParams.Model.Raw(), + RawRepresentation = createResult, + ResponseId = m.MessageId, + Usage = createResult.Usage is { } usage ? ToUsageDetails(usage) : null, + }; + } + + /// + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (messages is null) + { + throw new ArgumentNullException(nameof(messages)); + } + + List messageParams = CreateMessageParams(messages, out List? systemMessages); + MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); + + string? messageId = null; + string? modelID = null; + UsageDetails? usageDetails = null; + ChatFinishReason? finishReason = null; + Dictionary? streamingFunctions = null; + + await foreach (var createResult in this._anthropicClient.Messages.CreateStreaming(createParams, cancellationToken).WithCancellation(cancellationToken)) + { + List contents = []; + + switch (createResult.Value) + { + case RawMessageStartEvent rawMessageStart: + if (string.IsNullOrWhiteSpace(messageId)) + { + messageId = rawMessageStart.Message.ID; + } + + if (string.IsNullOrWhiteSpace(modelID)) + { + modelID = rawMessageStart.Message.Model; + } + + if (rawMessageStart.Message.Usage is { } usage) + { + UsageDetails current = ToUsageDetails(usage); + if (usageDetails is null) + { + usageDetails = current; + } + else + { + usageDetails.Add(current); + } + } + break; + + case RawMessageDeltaEvent rawMessageDelta: + finishReason = ToFinishReason(rawMessageDelta.Delta.StopReason); + if (rawMessageDelta.Usage is { } deltaUsage) + { + UsageDetails current = ToUsageDetails(deltaUsage); + if (usageDetails is null) + { + usageDetails = current; + } + else + { + usageDetails.Add(current); + } + } + break; + + case RawContentBlockStartEvent contentBlockStart: + switch (contentBlockStart.ContentBlock.Value) + { + case TextBlock text: + contents.Add(new TextContent(text.Text) + { + RawRepresentation = text, + }); + break; + + case ThinkingBlock thinking: + contents.Add(new TextReasoningContent(thinking.Thinking) + { + ProtectedData = thinking.Signature, + RawRepresentation = thinking, + }); + break; + + case RedactedThinkingBlock redactedThinking: + contents.Add(new TextReasoningContent(string.Empty) + { + ProtectedData = redactedThinking.Data, + RawRepresentation = redactedThinking, + }); + break; + + case ToolUseBlock toolUse: + streamingFunctions ??= []; + streamingFunctions[contentBlockStart.Index] = new() + { + CallId = toolUse.ID, + Name = toolUse.Name, + }; + break; + } + break; + + case RawContentBlockDeltaEvent contentBlockDelta: + switch (contentBlockDelta.Delta.Value) + { + case TextDelta textDelta: + contents.Add(new TextContent(textDelta.Text) + { + RawRepresentation = textDelta, + }); + break; + + case InputJSONDelta inputDelta: + if (streamingFunctions is not null && + streamingFunctions.TryGetValue(contentBlockDelta.Index, out StreamingFunctionData? functionData)) + { + functionData.Arguments.Append(inputDelta.PartialJSON); + } + break; + + case ThinkingDelta thinkingDelta: + contents.Add(new TextReasoningContent(thinkingDelta.Thinking) + { + RawRepresentation = thinkingDelta, + }); + break; + + case SignatureDelta signatureDelta: + contents.Add(new TextReasoningContent(null) + { + ProtectedData = signatureDelta.Signature, + RawRepresentation = signatureDelta, + }); + break; + } + break; + + case RawContentBlockStopEvent contentBlockStop: + if (streamingFunctions is not null) + { + foreach (var sf in streamingFunctions) + { + contents.Add(FunctionCallContent.CreateFromParsedArguments(sf.Value.Arguments.ToString(), sf.Value.CallId, sf.Value.Name, + json => JsonSerializer.Deserialize>(json, AnthropicClientJsonContext.Default.DictionaryStringObject))); + } + } + break; + } + + yield return new(ChatRole.Assistant, contents) + { + CreatedAt = DateTimeOffset.UtcNow, + FinishReason = finishReason, + MessageId = messageId, + ModelId = modelID, + RawRepresentation = createResult, + ResponseId = messageId, + }; + } + + if (usageDetails is not null) + { + yield return new(ChatRole.Assistant, [new UsageContent(usageDetails)]) + { + CreatedAt = DateTimeOffset.UtcNow, + FinishReason = finishReason, + MessageId = messageId, + ModelId = modelID, + ResponseId = messageId, + }; + } + } + + private static List CreateMessageParams(IEnumerable messages, out List? systemMessages) + { + List messageParams = []; + systemMessages = null; + + foreach (ChatMessage message in messages) + { + if (message.Role == ChatRole.System) + { + foreach (AIContent content in message.Contents) + { + if (content is TextContent tc) + { + (systemMessages ??= []).Add(new() { Text = tc.Text }); + } + } + + continue; + } + + List contents = []; + + foreach (AIContent content in message.Contents) + { + switch (content) + { + case AIContent ac when ac.RawRepresentation is ContentBlockParam rawContent: + contents.Add(rawContent); + break; + + case TextContent tc: + string text = tc.Text; + if (message.Role == ChatRole.Assistant) + { + text = text.TrimEnd(); + if (!string.IsNullOrWhiteSpace(text)) + { + contents.Add(new TextBlockParam() { Text = text }); + } + } + else if (!string.IsNullOrWhiteSpace(text)) + { + contents.Add(new TextBlockParam() { Text = text }); + } + break; + + case TextReasoningContent trc when !string.IsNullOrEmpty(trc.Text): + contents.Add(new ThinkingBlockParam() + { + Thinking = trc.Text, + Signature = trc.ProtectedData ?? string.Empty, + }); + break; + + case TextReasoningContent trc when !string.IsNullOrEmpty(trc.ProtectedData): + contents.Add(new RedactedThinkingBlockParam() + { + Data = trc.ProtectedData!, + }); + break; + + case DataContent dc when dc.HasTopLevelMediaType("image"): + contents.Add(new ImageBlockParam() + { + Source = new(new Base64ImageSource() { Data = dc.Base64Data.ToString(), MediaType = dc.MediaType }) + }); + break; + + case DataContent dc when string.Equals(dc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): + contents.Add(new DocumentBlockParam() + { + Source = new(new Base64PDFSource() { Data = dc.Base64Data.ToString() }), + }); + break; + + case DataContent dc when dc.HasTopLevelMediaType("text"): + contents.Add(new DocumentBlockParam() + { + Source = new(new PlainTextSource() { Data = Encoding.UTF8.GetString(dc.Data.ToArray()) }), + }); + break; + + case UriContent uc when uc.HasTopLevelMediaType("image"): + contents.Add(new ImageBlockParam() + { + Source = new(new URLImageSource() { URL = uc.Uri.AbsoluteUri }), + }); + break; + + case UriContent uc when string.Equals(uc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): + contents.Add(new DocumentBlockParam() + { + Source = new(new URLPDFSource() { URL = uc.Uri.AbsoluteUri }), + }); + break; + + case FunctionCallContent fcc: + contents.Add(new ToolUseBlockParam() + { + ID = fcc.CallId, + Name = fcc.Name, + Input = fcc.Arguments?.ToDictionary(e => e.Key, e => e.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(e.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? [], + }); + break; + + case FunctionResultContent frc: + contents.Add(new ToolResultBlockParam() + { + ToolUseID = frc.CallId, + IsError = frc.Exception is not null, + Content = new(JsonSerializer.Serialize(frc.Result, AnthropicClientJsonContext.Default.JsonElement)), + }); + break; + } + } + + if (contents.Count == 0) + { + continue; + } + + messageParams.Add(new() + { + Role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User, + Content = contents, + }); + } + + if (messageParams.Count == 0) + { + messageParams.Add(new() { Role = Role.User, Content = new("\u200b") }); // zero-width space + } + + return messageParams; + } + + private MessageCreateParams GetMessageCreateParams(List messages, List? systemMessages, ChatOptions? options) + { + // Get the initial MessageCreateParams, either with a raw representation provided by the options + // or with only the required properties set. + MessageCreateParams? createParams = options?.RawRepresentationFactory?.Invoke(this) as MessageCreateParams; + if (createParams is not null) + { + // Merge any messages preconfigured on the params with the ones provided to the IChatClient. + createParams = createParams with { Messages = [.. createParams.Messages, .. messages] }; + } + else + { + createParams = new MessageCreateParams() + { + MaxTokens = options?.MaxOutputTokens ?? this._defaultMaxTokens, + Messages = messages, + Model = options?.ModelId ?? this._defaultModelId ?? throw new InvalidOperationException("Model ID must be specified either in ChatOptions or as the default for the client."), + }; + } + + // Handle any other options to propagate to the create params. + if (options is not null) + { + if (options.Instructions is { } instructions) + { + (systemMessages ??= []).Add(new TextBlockParam() { Text = instructions }); + } + + if (options.StopSequences is { Count: > 0 } stopSequences) + { + createParams = createParams.StopSequences is { } existingSequences ? + createParams with { StopSequences = [.. existingSequences, .. stopSequences] } : + createParams with { StopSequences = [.. stopSequences] }; + } + + if (createParams.Temperature is null && options.Temperature is { } temperature) + { + createParams = createParams with { Temperature = temperature }; + } + + if (createParams.TopK is null && options.TopK is { } topK) + { + createParams = createParams with { TopK = topK }; + } + + if (createParams.TopP is null && options.TopP is { } topP) + { + createParams = createParams with { TopP = topP }; + } + + if (options.Tools is { } tools) + { + List? createdTools = createParams.Tools; + foreach (var tool in tools) + { + switch (tool) + { + case ToolUnionAITool raw: + (createdTools ??= []).Add(raw.Tool); + break; + + case AIFunctionDeclaration af: + Dictionary properties = []; + List required = []; + JsonElement inputSchema = af.JsonSchema; + if (inputSchema.ValueKind is JsonValueKind.Object) + { + if (inputSchema.TryGetProperty("properties", out JsonElement propsElement) && propsElement.ValueKind is JsonValueKind.Object) + { + foreach (JsonProperty p in propsElement.EnumerateObject()) + { + properties[p.Name] = p.Value; + } + } + + if (inputSchema.TryGetProperty("required", out JsonElement reqElement) && reqElement.ValueKind is JsonValueKind.Array) + { + foreach (JsonElement r in reqElement.EnumerateArray()) + { + if (r.ValueKind is JsonValueKind.String && r.GetString() is { } s && !string.IsNullOrWhiteSpace(s)) + { + required.Add(s); + } + } + } + } + + (createdTools ??= []).Add(new Tool() + { + Name = af.Name, + Description = af.Description, + InputSchema = new(properties) { Required = required }, + }); + break; + + case HostedWebSearchTool: + (createdTools ??= []).Add(new WebSearchTool20250305()); + break; + } + } + + if (createdTools?.Count > 0) + { + createParams = createParams with { Tools = createdTools }; + } + } + + if (createParams.ToolChoice is null && options.ToolMode is { } toolMode) + { + ToolChoice? toolChoice = + toolMode is AutoChatToolMode ? new ToolChoiceAuto() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : + toolMode is NoneChatToolMode ? new ToolChoiceNone() : + toolMode is RequiredChatToolMode ? new ToolChoiceAny() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : + (ToolChoice?)null; + if (toolChoice is not null) + { + createParams = createParams with { ToolChoice = toolChoice }; + } + } + } + + if (systemMessages is not null) + { + if (createParams.System is { } existingSystem) + { + if (existingSystem.Value is string existingMessage) + { + systemMessages.Insert(0, new TextBlockParam() { Text = existingMessage }); + } + else if (existingSystem.Value is IReadOnlyList existingMessages) + { + systemMessages.InsertRange(0, existingMessages); + } + } + + createParams = createParams with { System = systemMessages }; + } + + return createParams; + } + + private static UsageDetails ToUsageDetails(Usage usage) => + ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); + + private static UsageDetails ToUsageDetails(MessageDeltaUsage usage) => + ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); + + private static UsageDetails ToUsageDetails(long? inputTokens, long? outputTokens, long? cacheCreationInputTokens, long? cacheReadInputTokens) + { + UsageDetails usageDetails = new() + { + InputTokenCount = inputTokens, + OutputTokenCount = outputTokens, + TotalTokenCount = (inputTokens is not null || outputTokens is not null) ? (inputTokens ?? 0) + (outputTokens ?? 0) : null, + }; + + if (cacheCreationInputTokens is not null) + { + (usageDetails.AdditionalCounts ??= [])[nameof(Usage.CacheCreationInputTokens)] = cacheCreationInputTokens.Value; + } + + if (cacheReadInputTokens is not null) + { + (usageDetails.AdditionalCounts ??= [])[nameof(Usage.CacheReadInputTokens)] = cacheReadInputTokens.Value; + } + + return usageDetails; + } + + private static ChatFinishReason? ToFinishReason(ApiEnum? stopReason) => + stopReason?.Value() switch + { + null => null, + StopReason.Refusal => ChatFinishReason.ContentFilter, + StopReason.MaxTokens => ChatFinishReason.Length, + StopReason.ToolUse => ChatFinishReason.ToolCalls, + _ => ChatFinishReason.Stop, + }; + + private static AIContent ToAIContent(ContentBlock block) + { + switch (block.Value) + { + case TextBlock text: + TextContent tc = new(text.Text) + { + RawRepresentation = text, + }; + + if (text.Citations is { Count: > 0 }) + { + tc.Annotations = [.. text.Citations.Select(ToAIAnnotation).OfType()]; + } + + return tc; + + case ThinkingBlock thinking: + return new TextReasoningContent(thinking.Thinking) + { + ProtectedData = thinking.Signature, + RawRepresentation = thinking, + }; + + case RedactedThinkingBlock redactedThinking: + return new TextReasoningContent(string.Empty) + { + ProtectedData = redactedThinking.Data, + RawRepresentation = redactedThinking, + }; + + case ToolUseBlock toolUse: + return new FunctionCallContent( + toolUse.ID, + toolUse.Name, + toolUse.Properties.TryGetValue("input", out JsonElement element) ? + JsonSerializer.Deserialize>(element, AnthropicClientJsonContext.Default.DictionaryStringObject) : + null) + { + RawRepresentation = toolUse, + }; + + default: + return new AIContent() + { + RawRepresentation = block.Value, + }; + } + } + + private static AIAnnotation? ToAIAnnotation(TextCitation citation) + { + CitationAnnotation annotation = new() + { + Title = citation.Title ?? citation.DocumentTitle, + Snippet = citation.CitedText, + FileId = citation.FileID, + }; + + if (citation.TryPickCitationsWebSearchResultLocation(out var webSearchLocation)) + { + annotation.Url = Uri.TryCreate(webSearchLocation.URL, UriKind.Absolute, out Uri? url) ? url : null; + } + else if (citation.TryPickCitationsSearchResultLocation(out var searchLocation)) + { + annotation.Url = Uri.TryCreate(searchLocation.Source, UriKind.Absolute, out Uri? url) ? url : null; + } + + return annotation; + } + + private sealed class StreamingFunctionData + { + public string CallId { get; set; } = ""; + public string Name { get; set; } = ""; + public StringBuilder Arguments { get; } = new(); + } + } + + private sealed class ToolUnionAITool(ToolUnion tool) : AITool + { + public ToolUnion Tool => tool; + + public override string Name => tool.Value?.GetType().Name ?? base.Name; + + public override object? GetService(System.Type serviceType, object? serviceKey = null) => + serviceKey is null && serviceType?.IsInstanceOfType(tool) is true ? tool : + base.GetService(serviceType!, serviceKey); + } +} 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 index 1bdf8aa237..63211ce76e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -1,7 +1,8 @@ - + - net9.0 + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) preview enable true @@ -11,17 +12,17 @@ + - - Microsoft Agent Framework for Foundry Agents - Provides Microsoft Agent Framework support for Foundry Agents. + Microsoft Agent Framework for Anthropic Agents + Provides Microsoft Agent Framework support for Anthropic Agents. From b1f646652d88f81471666001dd021937fb5ae931 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:51:20 +0000 Subject: [PATCH 07/24] Public Preps --- .../AnthropicBetaChatClient.cs | 592 ------------------ .../AnthropicClientJsonContext.cs | 13 + .../External/AnthropicBetaClientExtensions.cs | 3 + .../External/AnthropicClientExtensions.cs | 3 +- 4 files changed, 18 insertions(+), 593 deletions(-) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientJsonContext.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs deleted file mode 100644 index 8a600ce1ba..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaChatClient.cs +++ /dev/null @@ -1,592 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -#pragma warning disable CA1812 - -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using Anthropic; -using Anthropic.Models.Beta.Messages; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Anthropic; - -/// -/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using -/// Azure-specific agent capabilities. -/// -internal sealed class AnthropicBetaChatClient : IChatClient -{ - private readonly AnthropicClient _client; - private readonly ChatClientMetadata _metadata; - - internal AnthropicBetaChatClient(AnthropicClient client, long defaultMaxTokens, Uri? endpoint = null, string? defaultModelId = null) - { - this._client = client; - this._metadata = new ChatClientMetadata(providerName: "anthropic", providerUri: endpoint ?? new Uri("https://api.anthropic.com"), defaultModelId); - this.DefaultMaxTokens = defaultMaxTokens; - } - - public long DefaultMaxTokens { get; set; } - - public void Dispose() - { - } - - public async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - var modelId = options?.ModelId ?? this._metadata.DefaultModelId - ?? throw new InvalidOperationException("No model ID specified in options or default model provided at the client initialization."); - - BetaMessage response = await this._client.Beta.Messages.Create(CreateBetaMessageParameters(this, modelId, messages, options), cancellationToken).ConfigureAwait(false); - - ChatMessage chatMessage = new(ChatRole.Assistant, ProcessResponseContent(response)); - - return new ChatResponse(chatMessage) - { - ResponseId = response.ID, - FinishReason = response.StopReason?.Value() switch - { - BetaStopReason.MaxTokens => ChatFinishReason.Length, - _ => ChatFinishReason.Stop, - }, - ModelId = response.Model, - RawRepresentation = response, - Usage = response.Usage is { } usage ? CreateUsageDetails(usage) : null - }; - } - - public object? GetService(System.Type serviceType, object? serviceKey = null) - { - return (serviceKey is null && serviceType == typeof(AnthropicClient)) - ? this._client - : (serviceKey is null && serviceType == typeof(ChatClientMetadata)) - ? this._metadata - : null; - } - - public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - /// Provides an wrapper for a . - internal sealed class BetaToolAITool(BetaTool tool) : AITool - { - public BetaTool Tool => tool; - public override string Name => this.Tool.GetType().Name; - - /// - public override object? GetService(System.Type serviceType, object? serviceKey = null) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is null && serviceType.IsInstanceOfType(this.Tool) ? this.Tool : - base.GetService(serviceType, serviceKey); - } - } - - /// - /// Create usage details from usage - /// - private static UsageDetails CreateUsageDetails(BetaUsage usage) - { - UsageDetails usageDetails = new() - { - InputTokenCount = usage.InputTokens, - OutputTokenCount = usage.OutputTokens, - AdditionalCounts = [], - }; - - if (usage.CacheCreationInputTokens.HasValue) - { - usageDetails.AdditionalCounts.Add(nameof(usage.CacheCreationInputTokens), usage.CacheCreationInputTokens.Value); - } - - if (usage.CacheReadInputTokens.HasValue) - { - usageDetails.AdditionalCounts.Add(nameof(usage.CacheReadInputTokens), usage.CacheReadInputTokens.Value); - } - - return usageDetails; - } - - private static InputSchema AIFunctionDeclarationToInputSchema(AIFunctionDeclaration function) - => new(function.JsonSchema.EnumerateObject().ToDictionary(k => k.Name, v => v.Value)); - - private static BetaThinkingConfigParam? GetThinkingParameters(ChatOptions? options) - { - if (options?.AdditionalProperties?.TryGetValue(nameof(BetaThinkingConfigParam), out var value) == true) - { - return value as BetaThinkingConfigParam; - } - - return null; - } - - private static List? GetMcpServers(ChatOptions? options) - { - List? mcpServerDefinitions = null; - - if (options?.Tools is { Count: > 0 }) - { -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - foreach (var mcpt in options.Tools.OfType()) - { - (mcpServerDefinitions ??= []).Add( - new BetaRequestMCPServerURLDefinition() - { - Name = mcpt.ServerName, - URL = mcpt.ServerAddress, - AuthorizationToken = mcpt.AuthorizationToken, - ToolConfiguration = new() { AllowedTools = mcpt.AllowedTools?.ToList() } - }); - } -#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - return mcpServerDefinitions; - } - - /// - /// Create message parameters from chat messages and options - /// - private static MessageCreateParams CreateBetaMessageParameters(AnthropicBetaChatClient client, string modelId, IEnumerable messages, ChatOptions? options) - { - List? tools = null; - BetaToolChoice? toolChoice = null; - - if (options?.Tools is { Count: > 0 }) - { - if (options.ToolMode is RequiredChatToolMode r) - { - toolChoice = r.RequiredFunctionName is null ? new BetaToolChoice(new BetaToolChoiceAny()) : new BetaToolChoice(new BetaToolChoiceTool(r.RequiredFunctionName)); - } - - tools = []; - foreach (var tool in options.Tools) - { - switch (tool) - { - case BetaToolAITool betaToolAiTool: - tools.Add(new BetaToolUnion(betaToolAiTool.Tool)); - break; - - case AIFunctionDeclaration f: - tools.Add(new BetaToolUnion(new BetaTool() - { - Name = f.Name, - Description = f.Description, - InputSchema = AIFunctionDeclarationToInputSchema(f) - })); - break; - - case HostedCodeInterpreterTool codeTool: - if (codeTool.AdditionalProperties?["version"] is string version && version.Contains("20250522")) - { - tools.Add(new BetaCodeExecutionTool20250522()); - } - else - { - tools.Add(new BetaCodeExecutionTool20250825()); - } - break; - - case HostedWebSearchTool webSearchTool: - tools.Add(new BetaToolUnion(new BetaWebSearchTool20250305() - { - MaxUses = (long?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.MaxUses)], - AllowedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.AllowedDomains)], - BlockedDomains = (List?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.BlockedDomains)], - CacheControl = (BetaCacheControlEphemeral?)webSearchTool.AdditionalProperties?[nameof(BetaWebSearchTool20250305.CacheControl)], - Name = JsonSerializer.Deserialize(JsonSerializer.Serialize(webSearchTool.Name, AnthropicClientJsonContext.Default.String), AnthropicClientJsonContext.Default.JsonElement), - UserLocation = (UserLocation?)webSearchTool.AdditionalProperties?[nameof(UserLocation)] - })); - break; - } - } - } - - MessageCreateParams? providedParameters = options?.RawRepresentationFactory?.Invoke(client) as MessageCreateParams; - - return new MessageCreateParams() - { - Model = modelId, - Messages = GetMessages(messages), - System = GetSystem(options, messages), - MaxTokens = (options?.MaxOutputTokens is int maxOutputTokens) ? maxOutputTokens : providedParameters?.MaxTokens ?? client.DefaultMaxTokens, - Temperature = (options?.Temperature is float temperature) ? (double)temperature : providedParameters?.Temperature, - TopP = (options?.TopP is float topP) ? (double)topP : providedParameters?.TopP, - TopK = (options?.TopK is int topK) ? topK : providedParameters?.TopK, - StopSequences = (options?.StopSequences is { Count: > 0 } stopSequences) ? stopSequences.ToList() : providedParameters?.StopSequences, - ToolChoice = toolChoice ?? providedParameters?.ToolChoice, - Tools = tools ?? providedParameters?.Tools, - Thinking = GetThinkingParameters(options) ?? providedParameters?.Thinking, - MCPServers = GetMcpServers(options) ?? providedParameters?.MCPServers, - }; - } - - private static SystemModel? GetSystem(ChatOptions? options, IEnumerable messages) - { - StringBuilder? fullInstructions = (options?.Instructions is string instructions) ? new(instructions) : null; - - foreach (ChatMessage message in messages) - { - if (message.Role == ChatRole.System) - { - (fullInstructions ??= new()).AppendLine(string.Concat(message.Contents.OfType())); - } - } - - return fullInstructions is not null ? new SystemModel(fullInstructions.ToString()) : null; - } - - private static List GetMessages(IEnumerable chatMessages) - { - List betaMessages = []; - - foreach (ChatMessage message in chatMessages) - { - if (message.Role == ChatRole.System) - { - continue; - } - - // Process contents in order, creating new messages when switching between tool results and other content - // This preserves ordering and handles interleaved tool calls, AI output, and tool results - BetaMessageParam? currentMessage = null; - bool lastWasToolResult = false; - - for (var currentIndex = 0; currentIndex < message.Contents.Count; currentIndex++) - { - bool isToolResult = message.Contents[currentIndex] is FunctionResultContent; - - // Create new message if: - // 1. This is the first content item, OR - // 2. We're switching between tool result and non-tool result content - if (currentMessage == null || lastWasToolResult != isToolResult) - { - var messageRole = isToolResult ? Role.User : (message.Role == ChatRole.Assistant ? Role.Assistant : Role.User); - currentMessage = new() - { - // Tool results must always be in User messages, others respect original role - Role = messageRole, - Content = new BetaMessageParamContent(GetContents(message.Contents, currentIndex, messageRole)) - }; - betaMessages.Add(currentMessage); - lastWasToolResult = isToolResult; - } - } - } - - betaMessages.RemoveAll(m => m.Content.TryPickBetaContentBlockParams(out var blocks) && blocks.Count == 0); - - // Avoid errors from completely empty input. - if (betaMessages.Count == 0) - { - betaMessages.Add(new BetaMessageParam() { Role = Role.User, Content = "\u200b" }); // zero-width space - } - - return betaMessages; - } - - private static List GetContents(IList contents, int currentIndex, Role currentRole) - { - bool addedToolResult = false; - List contentBlocks = []; - for (var i = currentIndex; i < contents.Count; i++) - { - switch (contents[i]) - { - case FunctionResultContent frc: - if (addedToolResult) - { - // Any subsequent function result needs to be processed as a new message - goto end; - } - addedToolResult = true; - contentBlocks.Add(new BetaToolResultBlockParam(frc.CallId) - { - Content = new BetaToolResultBlockParamContent(frc.Result?.ToString() ?? string.Empty), - IsError = frc.Exception is not null, - CacheControl = frc.AdditionalProperties?[nameof(BetaToolResultBlockParam.CacheControl)] as BetaCacheControlEphemeral, - ToolUseID = frc.CallId, - }); - break; - - case FunctionCallContent fcc: - contentBlocks.Add(new BetaToolUseBlockParam() - { - ID = fcc.CallId, - Name = fcc.Name, - Input = fcc.Arguments?.ToDictionary(k => k.Key, v => JsonSerializer.SerializeToElement(v.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? new Dictionary() - }); - break; - - case TextReasoningContent reasoningContent: - if (string.IsNullOrEmpty(reasoningContent.Text)) - { - contentBlocks.Add(new BetaRedactedThinkingBlockParam(reasoningContent.ProtectedData!)); - } - else - { - contentBlocks.Add(new BetaThinkingBlockParam() - { - Signature = reasoningContent.ProtectedData!, - Thinking = reasoningContent.Text, - }); - } - break; - - case TextContent textContent: - string text = textContent.Text; - if (currentRole == Role.Assistant) - { - var trimmedText = text.TrimEnd(); - if (!string.IsNullOrEmpty(trimmedText)) - { - contentBlocks.Add(new BetaTextBlockParam() { Text = trimmedText }); - } - } - else if (!string.IsNullOrWhiteSpace(text)) - { - contentBlocks.Add(new BetaTextBlockParam() { Text = text }); - } - - break; - - case HostedFileContent hostedFileContent: - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaFileDocumentSource(hostedFileContent.FileId))))); - break; - - case DataContent imageContent when imageContent.HasTopLevelMediaType("image"): - contentBlocks.Add( - new BetaImageBlockParam( - new BetaImageBlockParamSource( - new BetaBase64ImageSource() - { - Data = Convert.ToBase64String(imageContent.Data.ToArray()), - MediaType = imageContent.MediaType - }))); - break; - - case DataContent pdfDocumentContent when pdfDocumentContent.MediaType == "application/pdf": - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaBase64PDFSource() - { - Data = Convert.ToBase64String(pdfDocumentContent.Data.ToArray()), - })))); - break; - - case DataContent textDocumentContent when textDocumentContent.HasTopLevelMediaType("text"): - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaPlainTextSource() - { - Data = Convert.ToBase64String(textDocumentContent.Data.ToArray()), - })))); - break; - - case UriContent imageUriContent when imageUriContent.HasTopLevelMediaType("image"): - contentBlocks.Add( - new BetaImageBlockParam( - new BetaImageBlockParamSource( - new BetaURLImageSource(imageUriContent.Uri.ToString())))); - break; - - case UriContent pdfUriContent when pdfUriContent.MediaType == "application/pdf": - contentBlocks.Add( - new BetaContentBlockParam( - new BetaRequestDocumentBlock( - new BetaRequestDocumentBlockSource( - new BetaURLPDFSource(pdfUriContent.Uri.ToString()))))); - break; - } - } - -end: - return contentBlocks; - } - - /// - /// Process response content - /// - private static List ProcessResponseContent(BetaMessage response) - { - List contents = new(); - - foreach (BetaContentBlock content in response.Content) - { - switch (content) - { - case BetaContentBlock ct when ct.TryPickThinking(out var thinkingBlock): - contents.Add(new TextReasoningContent(thinkingBlock.Thinking) - { - ProtectedData = thinkingBlock.Signature, - }); - break; - - case BetaContentBlock ct when ct.TryPickRedactedThinking(out var redactedThinkingBlock): - contents.Add(new TextReasoningContent(null) - { - ProtectedData = redactedThinkingBlock.Data, - }); - break; - - case BetaContentBlock ct when ct.TryPickText(out var textBlock): - var textContent = new TextContent(textBlock.Text); - if (textBlock.Citations is { Count: > 0 }) - { - foreach (var tau in textBlock.Citations) - { - var annotation = new CitationAnnotation() - { - RawRepresentation = tau, - Snippet = tau.CitedText, - FileId = tau.Title, - AnnotatedRegions = [] - }; - - switch (tau) - { - case BetaTextCitation bChar when bChar.TryPickCitationCharLocation(out var charLocation): - { - annotation.AnnotatedRegions.Add(new TextSpanAnnotatedRegion { StartIndex = (int?)charLocation?.StartCharIndex, EndIndex = (int?)charLocation?.EndCharIndex }); - break; - } - - case BetaTextCitation search when search.TryPickCitationSearchResultLocation(out var searchLocation) && Uri.IsWellFormedUriString(searchLocation.Source, UriKind.RelativeOrAbsolute): - { - annotation.Url = new Uri(searchLocation.Source); - break; - } - - case BetaTextCitation search when search.TryPickCitationsWebSearchResultLocation(out var searchLocation): - { - annotation.Url = new Uri(searchLocation.URL); - break; - } - - default: - { - (textContent.Annotations ?? []).Add(new CitationAnnotation - { - Snippet = tau.CitedText, - Title = tau.Title, - RawRepresentation = tau - }); - break; - } - } - - (textContent.Annotations ??= []).Add(annotation); - } - } - contents.Add(textContent); - break; - - case BetaContentBlock ct when ct.TryPickToolUse(out var toolUse): - contents.Add(new FunctionCallContent(toolUse.ID, toolUse.Name) - { - Arguments = toolUse.Input?.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), - RawRepresentation = toolUse - }); - break; - - case BetaContentBlock ct when ct.TryPickMCPToolUse(out var mcpToolUse): -#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - contents.Add(new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) - { - Arguments = mcpToolUse.Input.ToDictionary(kv => kv.Key, kv => (object?)kv.Value), - RawRepresentation = mcpToolUse - }); - break; - - case BetaContentBlock ct when ct.TryPickMCPToolResult(out var mcpToolResult): - { - contents.Add(new McpServerToolResultContent(mcpToolResult.ToolUseID) - { - Output = [mcpToolResult.IsError - ? new ErrorContent(mcpToolResult.Content.Value.ToString()) - : new TextContent(mcpToolResult.Content.Value.ToString())], - RawRepresentation = mcpToolResult - }); - break; - } - - case BetaContentBlock ct when ct.TryPickCodeExecutionToolResult(out var cer): - { - var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; - if (cer.Content.TryPickError(out var cerErr)) - { - codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = cerErr.ErrorCode.Value().ToString() }); - } - if (cer.Content.TryPickResultBlock(out var cerResult)) - { - codeResult.Outputs.Add(new TextContent(cerResult.Stdout) { RawRepresentation = cerResult }); - if (!string.IsNullOrWhiteSpace(cerResult.Stderr)) - { - codeResult.Outputs.Add(new TextContent(cerResult.Stderr) { RawRepresentation = cerResult }); - } - - if (cerResult.Content is { Count: > 0 }) - { - foreach (var ceOutputContent in cerResult.Content) - { - codeResult.Outputs.Add(new HostedFileContent(ceOutputContent.FileID)); - } - } - } - - contents.Add(codeResult); - break; - } - - case BetaContentBlock ct when ct.TryPickBashCodeExecutionToolResult(out var bashCer): - { - var codeResult = new CodeInterpreterToolResultContent() { Outputs = [] }; - if (bashCer.Content.TryPickBetaBashCodeExecutionToolResultError(out var bashCerErr)) - { - codeResult.Outputs.Add(new ErrorContent(null) { ErrorCode = bashCerErr.ErrorCode.Value().ToString() }); - } - if (bashCer.Content.TryPickBetaBashCodeExecutionResultBlock(out var bashCerResult)) - { - codeResult.Outputs.Add(new TextContent(bashCerResult.Stdout) { RawRepresentation = bashCerResult }); - if (!string.IsNullOrWhiteSpace(bashCerResult.Stderr)) - { - codeResult.Outputs.Add(new TextContent(bashCerResult.Stderr) { RawRepresentation = bashCerResult }); - } - } - - contents.Add(codeResult); - break; - } -#pragma warning restore MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - - default: - { - contents.Add(new AIContent { RawRepresentation = content }); - break; - } - } - } - - return contents; - } -} - -[JsonSerializable(typeof(JsonElement))] -[JsonSerializable(typeof(string))] -[JsonSerializable(typeof(Dictionary))] -internal sealed partial class AnthropicClientJsonContext : JsonSerializerContext; 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/External/AnthropicBetaClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs index 5cd3521ca8..33671a704e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs @@ -1,5 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +// Adapted polyfill from https://raw.githubusercontent.com/stephentoub/anthropic-sdk-csharp/3034edde7c21ac1650b3358a7812b59685eff3a9/src/Anthropic/Services/Beta/Messages/AnthropicBetaClientExtensions.cs +// To be deleted once PR is Merged: https://github.com/anthropics/anthropic-sdk-csharp/pull/10 + using System.Globalization; using System.Runtime.CompilerServices; using System.Text; diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs index 15eac3ce06..92e60b98fd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -// Pollyfil from +// Adapted polyfill from https://raw.githubusercontent.com/stephentoub/anthropic-sdk-csharp/3034edde7c21ac1650b3358a7812b59685eff3a9/src/Anthropic/AnthropicClientExtensions.cs +// To be deleted once PR is Merged: https://github.com/anthropics/anthropic-sdk-csharp/pull/10 using System.Runtime.CompilerServices; using System.Text; From d22c39ec455158349602ce6f391fd10b51792327 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:47:01 +0000 Subject: [PATCH 08/24] UT + IT working --- dotnet/agent-framework-dotnet.slnx | 3 + .../Agent_With_Anthropic.csproj | 2 + .../Agent_With_Anthropic/Program.cs | 46 ++- .../AnthropicBetaServiceExtensions.cs | 97 ++++++ .../AnthropicClientExtensions.cs | 67 ++-- .../External/AnthropicBetaClientExtensions.cs | 26 +- .../AnthropicConfiguration.cs | 17 ++ ...opicChatCompletion.IntegrationTests.csproj | 22 ++ ...pletionChatClientAgentRunStreamingTests.cs | 15 + ...icChatCompletionChatClientAgentRunTests.cs | 15 + .../AnthropicChatCompletionFixture.cs | 80 +++++ ...nthropicChatCompletionRunStreamingTests.cs | 15 + .../AnthropicChatCompletionRunTests.cs | 15 + .../AnthropicBetaServiceExtensionsTests.cs | 285 ++++++++++++++++++ .../AnthropicClientExtensionsTests.cs | 257 ++++++++++++++++ ...osoft.Agents.AI.Anthropic.UnitTests.csproj | 11 + 16 files changed, 929 insertions(+), 44 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs create mode 100644 dotnet/src/Shared/IntegrationTests/AnthropicConfiguration.cs create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs create mode 100644 dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d4979b0500..8f884d4a2b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -316,6 +316,7 @@ + @@ -363,6 +364,7 @@ + @@ -379,6 +381,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 index 15fe5d99c5..ccc96d4fb1 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 @@ -10,6 +10,8 @@ + + diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index f6ce9d2b84..2d7f71898c 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -1,21 +1,55 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable CA1050 // Declare types in namespaces + // This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend. using Anthropic; -using Anthropic.Client; -using Anthropic.Core; +using Anthropic.Foundry; +using Azure.Core; +using Azure.Identity; using Microsoft.Agents.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 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"; -var client = new AnthropicClient(new ClientOptions { APIKey = apiKey }); +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(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey are provided, use Foundry with ApiKey authentication + : new AnthropicFoundryClient(new AzureTokenCredential(resource, new AzureCliCredential())); // Otherwise, use Foundry with Azure Client authentication -AIAgent agent = client.CreateAIAgent(model: model, instructions: JokerInstructions, name: JokerName); +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.")); + +public class AzureTokenCredential : IAnthropicFoundryCredentials +#pragma warning restore CA1050 // Declare types in namespaces +{ + private readonly TokenCredential _tokenCredential; + + public AzureTokenCredential(string resourceName, TokenCredential tokenCredential) + { + this.ResourceName = resourceName; + this._tokenCredential = tokenCredential; + } + + public string ResourceName { get; } + + public void Apply(HttpRequestMessage requestMessage) + { + var accessToken = this._tokenCredential.GetToken( + new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), + cancellationToken: CancellationToken.None); + + requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken.Token); + } +} 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..a61b332c33 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Anthropic.Services; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// 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 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/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index 61c92a2f6d..dc1338d8cf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using Anthropic; -using Anthropic.Services; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -14,7 +15,7 @@ public static class AnthropicClientExtensions /// /// Specifies the default maximum number of tokens allowed for processing operations. /// - public static long DefaultMaxTokens { get; set; } = 4096; + public static int DefaultMaxTokens { get; set; } = 4096; /// /// Creates a new AI agent using the specified model and options. @@ -26,6 +27,9 @@ public static class AnthropicClientExtensions /// 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, @@ -34,7 +38,10 @@ public static ChatClientAgent CreateAIAgent( string? name = null, string? description = null, IList? tools = null, - int? defaultMaxTokens = null) + int? defaultMaxTokens = null, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) { var options = new ChatClientAgentOptions { @@ -48,41 +55,43 @@ public static ChatClientAgent CreateAIAgent( options.ChatOptions = new ChatOptions { Tools = tools }; } - return new ChatClientAgent(client.AsIChatClient(model, defaultMaxTokens), options); + var chatClient = client.AsIChatClient(model, defaultMaxTokens ?? DefaultMaxTokens); + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + return new ChatClientAgent(chatClient, options, loggerFactory, services); } /// - /// Creates a new AI agent using the specified model and options. + /// Creates an AI agent from an using the Anthropic Chat Completion API. /// - /// 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. - /// The created AI agent. + /// 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, - string model, - string? instructions = null, - string? name = null, - string? description = null, - IList? tools = null, - int? defaultMaxTokens = null) + this IAnthropicClient client, + ChatClientAgentOptions options, + Func? clientFactory = null, + ILoggerFactory? loggerFactory = null, + IServiceProvider? services = null) { - var options = new ChatClientAgentOptions - { - Instructions = instructions, - Name = name, - Description = description, - }; + Throw.IfNull(client); + Throw.IfNull(options); - if (tools is { Count: > 0 }) + var chatClient = client.AsIChatClient(); + + if (clientFactory is not null) { - options.ChatOptions = new ChatOptions { Tools = tools }; + chatClient = clientFactory(chatClient); } - return new ChatClientAgent(betaService.AsIChatClient(model, defaultMaxTokens), options); + return new ChatClientAgent(chatClient, options, loggerFactory, services); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs index 33671a704e..f4defd7c96 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs @@ -85,12 +85,13 @@ public static AITool AsAITool(this BetaToolUnion tool) return new ToolUnionAITool(tool); } - private sealed class AnthropicChatClient( - IBetaService betaService, - string? defaultModelId, - int? defaultMaxOutputTokens) : IChatClient + private sealed class AnthropicChatClient : IChatClient { private const int DefaultMaxTokens = 1024; + private readonly IBetaService _betaService; + private readonly string? _defaultModelId; + private readonly int _defaultMaxTokens; + private readonly IAnthropicClient _anthropicClient; private static readonly AIJsonSchemaTransformCache s_transformCache = new(new AIJsonSchemaTransformOptions { @@ -116,12 +117,19 @@ private sealed class AnthropicChatClient( }, }); - private readonly IBetaService _betaService = betaService; - private readonly string? _defaultModelId = defaultModelId; - private readonly int _defaultMaxTokens = defaultMaxOutputTokens ?? DefaultMaxTokens; - private readonly IAnthropicClient _anthropicClient = typeof(BetaService) - .GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! + public AnthropicChatClient( + IBetaService betaService, + string? defaultModelId, + int? defaultMaxOutputTokens) + { + this._betaService = betaService; + this._defaultModelId = defaultModelId; + this._defaultMaxTokens = defaultMaxOutputTokens ?? DefaultMaxTokens; +#pragma warning disable IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. + this._anthropicClient = betaService.GetType().GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! .GetValue(betaService) as IAnthropicClient ?? throw new InvalidOperationException("Unable to access Anthropic client from BetaService."); +#pragma warning restore IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. + } private ChatClientMetadata? _metadata; 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..6c4ab3142a --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + True + + + + + + + + + + + + + + + + + diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs new file mode 100644 index 0000000000..961088c7ca --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace AnthropicChatCompletion.IntegrationTests; + +public class AnthropicChatCompletionChatClientAgentRunStreamingTests() + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false)) +{ +} + +public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true)) +{ +} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs new file mode 100644 index 0000000000..8515761a49 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace AnthropicChatCompletion.IntegrationTests; + +public class AnthropicChatCompletionChatClientAgentRunTests() + : ChatClientAgentRunTests(() => new(useReasoningChatModel: false)) +{ +} + +public class AnthropicChatCompletionChatClientAgentReasoningRunTests() + : ChatClientAgentRunTests(() => new(useReasoningChatModel: true)) +{ +} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs new file mode 100644 index 0000000000..3fa85567e4 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -0,0 +1,80 @@ +// 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 Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Shared.IntegrationTests; + +namespace AnthropicChatCompletion.IntegrationTests; + +public class AnthropicChatCompletionFixture : IChatClientAgentFixture +{ + private static readonly AnthropicConfiguration s_config = TestConfiguration.LoadSection(); + private readonly bool _useReasoningModel; + + private ChatClientAgent _agent = null!; + + public AnthropicChatCompletionFixture(bool useReasoningChatModel) + { + this._useReasoningModel = useReasoningChatModel; + } + + 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 chatClient = new AnthropicClient() { APIKey = s_config.ApiKey } + .AsIChatClient(defaultModelId: this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId) + .AsBuilder() + .ConfigureOptions(options + => options.RawRepresentationFactory = _ + => new MessageCreateParams() + { + Model = options.ModelId!, + MaxTokens = options.MaxOutputTokens ?? 4096, + Messages = [], + Thinking = this._useReasoningModel + ? new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(2048)) + : new BetaThinkingConfigParam(new BetaThinkingConfigDisabled()) + }).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..af05f8e612 --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace AnthropicChatCompletion.IntegrationTests; + +public class AnthropicChatCompletionRunStreamingTests() + : RunStreamingTests(() => new(useReasoningChatModel: false)) +{ +} + +public class AnthropicChatCompletionReasoningRunStreamingTests() + : RunStreamingTests(() => new(useReasoningChatModel: true)) +{ +} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs new file mode 100644 index 0000000000..2f84e5c61f --- /dev/null +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace AnthropicChatCompletion.IntegrationTests; + +public class AnthropicChatCompletionRunTests() + : RunTests(() => new(useReasoningChatModel: false)) +{ +} + +public class AnthropicChatCompletionReasoningRunTests() + : RunTests(() => new(useReasoningChatModel: true)) +{ +} 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..f0348a358d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -0,0 +1,285 @@ +// 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; + +namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions; + +/// +/// Unit tests for the 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; } + + 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.IMessageService Messages => throw new NotImplementedException(); + + public global::Anthropic.Services.Beta.IFileService Files => throw new NotImplementedException(); + + public global::Anthropic.Services.Beta.ISkillService Skills => throw new NotImplementedException(); + + 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..37b1bd03dd --- /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 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..7344b8bf7f --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Microsoft.Agents.AI.Anthropic.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + $(ProjectsTargetFrameworks) + + + + + + + From faf883184a3288f40423058f199402ca162c97e4 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:40:08 +0000 Subject: [PATCH 09/24] Update documentation + samples --- .../Agent_With_Anthropic/README.md | 47 +++++++++++-- .../GettingStarted/AgentProviders/README.md | 1 + .../Agent_Anthropic_Step01_Running/README.md | 42 ++++++++++++ .../Program.cs | 35 ++++++---- .../README.md | 45 +++++++++++++ .../README.md | 22 +++---- .../AgentWithAnthropic/README.md | 66 +++++++++++++++++-- dotnet/samples/GettingStarted/README.md | 1 + 8 files changed, 223 insertions(+), 36 deletions(-) create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md create mode 100644 dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md index df0854ba2f..afcf391572 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/README.md @@ -1,16 +1,53 @@ -# Prerequisites +# 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 -- Azure CLI installed and authenticated (for Azure credential authentication) +- Anthropic API key + +Set the following environment variables: -**Note**: This demo uses Azure CLI credentials for authentication. 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). +```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:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint -$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +$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/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/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md new file mode 100644 index 0000000000..a10b03bb5b --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md @@ -0,0 +1,42 @@ +# 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 +``` + +## 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/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs index 2532aa606c..a0506852a6 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -4,26 +4,35 @@ using Anthropic; using Anthropic.Core; -using Anthropic.Models.Beta.Messages; +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 apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); var model = Environment.GetEnvironmentVariable("ANTHROPIC_MODEL") ?? "claude-haiku-4-5"; - -var client = new AnthropicClient(new ClientOptions { APIKey = apiKey }).AsIChatClient(model).AsBuilder() - .ConfigureOptions( - o => (o.AdditionalProperties ??= []) - .Add(nameof(BetaThinkingConfigParam), new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(budgetTokens: 2048)))) - .Build(); - -AIAgent agent = new ChatClientAgent(client); +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(string.Join("\n", response.Messages.SelectMany(m => m.Contents.OfType().Select(c => c.Text)))); +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 ####"); @@ -40,11 +49,11 @@ { if (item is TextReasoningContent reasoningContent) { - Console.Write($"\e[97m{reasoningContent.Text}\e[0m"); + Console.WriteLine($"\e[92m{reasoningContent.Text}\e[0m"); } else if (item is TextContent textContent) { - Console.Write(textContent.Text); + 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..f01ce88b30 --- /dev/null +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md @@ -0,0 +1,45 @@ +# 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 +``` + +## 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/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md index 934373aa80..abe2496293 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md @@ -1,39 +1,37 @@ -# Using Function Tools with AI Agents +# Using Function Tools with Anthropic agents -This sample demonstrates how to use function tools with AI agents, allowing agents to call custom functions to retrieve information. +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 AI agent +- 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 (creation and deletion) +- Managing agent lifecycle ## Prerequisites Before you begin, ensure you have the following prerequisites: - .NET 8.0 SDK or later -- Azure Foundry service endpoint and deployment configured -- Azure CLI installed and authenticated (for Azure credential authentication) +- Anthropic API key configured -**Note**: This demo uses Azure CLI credentials for authentication. 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). +**Note**: This sample uses Anthropic Claude models. For more information, see [Anthropic documentation](https://docs.anthropic.com/). Set the following environment variables: ```powershell -$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint -$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +$env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic API key ``` ## Run the sample -Navigate to the FoundryAgents sample directory and run: +Navigate to the AgentWithAnthropic sample directory and run: ```powershell -cd dotnet/samples/GettingStarted/FoundryAgents -dotnet run --project .\FoundryAgents_Step03.1_UsingFunctionTools +cd dotnet/samples/GettingStarted/AgentWithAnthropic +dotnet run --project .\Agent_Anthropic_Step03_UsingFunctionTools ``` ## Expected behavior diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md index 4ed609ae81..fc1b73141c 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md @@ -1,14 +1,68 @@ -# Agent Framework with OpenAI +# Getting started with agents using Anthropic -These samples show how to use the Agent Framework with the OpenAI exchange types. +The getting started with agents using Anthropic samples demonstrate the fundamental concepts and functionalities +of single agents using Anthropic as the AI provider. -By default, the .Net version of Agent Framework uses the [Microsoft.Extensions.AI.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/) exchange types. +These samples use Anthropic Claude models as the AI provider and use ChatCompletion as the type of service. -For developers who are using the [OpenAI SDK](https://www.nuget.org/packages/OpenAI) this can be problematic because there are conflicting exchange types which can cause confusion. +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. -Agent Framework provides additional support to allow OpenAI developers to use the OpenAI exchange types. +## 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/). + +## Samples |Sample|Description| |---|---| -|[Creating an AIAgent](./Agent_OpenAI_Step01_Running/)|This sample demonstrates how to create and run a basic agent instructions with native OpenAI SDK types.| +|[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| From b7d548cdb71318f1b1645b52a2fe416747173ad6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:40:47 +0000 Subject: [PATCH 10/24] Update variable --- .../Agent_Anthropic_Step02_Reasoning/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs index a0506852a6..d362a9dd0d 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/Program.cs @@ -8,7 +8,7 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; -var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_APIKEY") ?? throw new InvalidOperationException("ANTHROPIC_API_KEY is not set."); +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; From e6004a07dfc337021e4c524e3220b6449f0fdac5 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:42:38 +0000 Subject: [PATCH 11/24] Revert nuget.config --- dotnet/nuget.config | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/nuget.config b/dotnet/nuget.config index a8522c3741..76d943ce16 100644 --- a/dotnet/nuget.config +++ b/dotnet/nuget.config @@ -3,7 +3,6 @@ - From ffbaf27fd7f6a953fd27f40e0b7659dc229b4c0e Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:11:41 +0000 Subject: [PATCH 12/24] Add IT for BetaService implementation --- ...pletionChatClientAgentRunStreamingTests.cs | 14 ++--- ...icChatCompletionChatClientAgentRunTests.cs | 14 ++--- .../AnthropicChatCompletionFixture.cs | 52 +++++++++++++------ ...nthropicChatCompletionRunStreamingTests.cs | 14 ++--- .../AnthropicChatCompletionRunTests.cs | 14 ++--- 5 files changed, 69 insertions(+), 39 deletions(-) diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs index 961088c7ca..fd05014471 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -4,12 +4,14 @@ namespace AnthropicChatCompletion.IntegrationTests; +public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); + +public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); + public class AnthropicChatCompletionChatClientAgentRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false)) -{ -} + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true)) -{ -} + : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs index 8515761a49..db150a2605 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -4,12 +4,14 @@ namespace AnthropicChatCompletion.IntegrationTests; +public class AnthropicBetaChatCompletionChatClientAgentRunTests() + : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: true)); + +public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests() + : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: true)); + public class AnthropicChatCompletionChatClientAgentRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: false)) -{ -} + : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionChatClientAgentReasoningRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: true)) -{ -} + : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs index 3fa85567e4..78f0189f85 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -7,6 +7,7 @@ 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; @@ -17,12 +18,14 @@ public class AnthropicChatCompletionFixture : IChatClientAgentFixture { private static readonly AnthropicConfiguration s_config = TestConfiguration.LoadSection(); private readonly bool _useReasoningModel; + private readonly bool _useBeta; private ChatClientAgent _agent = null!; - public AnthropicChatCompletionFixture(bool useReasoningChatModel) + public AnthropicChatCompletionFixture(bool useReasoningChatModel, bool useBeta) { this._useReasoningModel = useReasoningChatModel; + this._useBeta = useBeta; } public AIAgent Agent => this._agent; @@ -41,20 +44,39 @@ public Task CreateChatClientAgentAsync( string instructions = "You are a helpful assistant.", IList? aiTools = null) { - var chatClient = new AnthropicClient() { APIKey = s_config.ApiKey } - .AsIChatClient(defaultModelId: this._useReasoningModel ? s_config.ChatReasoningModelId : s_config.ChatModelId) - .AsBuilder() - .ConfigureOptions(options - => options.RawRepresentationFactory = _ - => new MessageCreateParams() - { - Model = options.ModelId!, - MaxTokens = options.MaxOutputTokens ?? 4096, - Messages = [], - Thinking = this._useReasoningModel - ? new BetaThinkingConfigParam(new BetaThinkingConfigEnabled(2048)) - : new BetaThinkingConfigParam(new BetaThinkingConfigDisabled()) - }).Build(); + 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() { diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs index af05f8e612..ee39281ba6 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -4,12 +4,14 @@ namespace AnthropicChatCompletion.IntegrationTests; +public class AnthropicBetaChatCompletionRunStreamingTests() + : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); + +public class AnthropicBetaChatCompletionReasoningRunStreamingTests() + : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); + public class AnthropicChatCompletionRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: false)) -{ -} + : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: true)) -{ -} + : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs index 2f84e5c61f..6cf514e695 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -4,12 +4,14 @@ namespace AnthropicChatCompletion.IntegrationTests; +public class AnthropicBetaChatCompletionRunTests() + : RunTests(() => new(useReasoningChatModel: false, useBeta: true)); + +public class AnthropicBetaChatCompletionReasoningRunTests() + : RunTests(() => new(useReasoningChatModel: true, useBeta: true)); + public class AnthropicChatCompletionRunTests() - : RunTests(() => new(useReasoningChatModel: false)) -{ -} + : RunTests(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunTests() - : RunTests(() => new(useReasoningChatModel: true)) -{ -} + : RunTests(() => new(useReasoningChatModel: true, useBeta: false)); From e804946378ca9ba2ea0d2da13baf1536368cb451 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:48:12 +0000 Subject: [PATCH 13/24] Remove polyfill + enable IT to run for netstandard 2.0 --- dotnet/Directory.Packages.props | 4 +- .../External/AnthropicBetaClientExtensions.cs | 948 ------------------ .../External/AnthropicClientExtensions.cs | 737 -------------- ...opicChatCompletion.IntegrationTests.csproj | 3 +- 4 files changed, 4 insertions(+), 1688 deletions(-) delete mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 4603f68d1b..04060b75fb 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -11,8 +11,8 @@ - - + + diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs deleted file mode 100644 index f4defd7c96..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicBetaClientExtensions.cs +++ /dev/null @@ -1,948 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Adapted polyfill from https://raw.githubusercontent.com/stephentoub/anthropic-sdk-csharp/3034edde7c21ac1650b3358a7812b59685eff3a9/src/Anthropic/Services/Beta/Messages/AnthropicBetaClientExtensions.cs -// To be deleted once PR is Merged: https://github.com/anthropics/anthropic-sdk-csharp/pull/10 - -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using Anthropic; -using Anthropic.Core; -using Anthropic.Models.Beta.Messages; -using Anthropic.Services; -using Microsoft.Agents.AI.Anthropic; - -#pragma warning disable MEAI001 // [Experimental] APIs in Microsoft.Extensions.AI -#pragma warning disable IDE0130 // Namespace does not match folder structure - -namespace Microsoft.Extensions.AI; - -/// -/// Provides extension methods for working with Anthropic beta services, including conversion to chat clients and tool -/// wrapping for use with Anthropic-specific chat implementations. -/// -public static class AnthropicBetaClientExtensions -{ - /// Gets an for use with this . - /// The beta service. - /// - /// The default ID of the model to use. - /// If , it must be provided per request via . - /// - /// - /// The default maximum number of tokens to generate in a response. - /// This may be overridden with . - /// If no value is provided for this parameter or in , a default maximum will be used. - /// - /// An that can be used to converse via the . - /// is . - public static IChatClient AsIChatClient( - this IBetaService betaService, - string? defaultModelId = null, - int? defaultMaxOutputTokens = null) - { - if (betaService is null) - { - throw new ArgumentNullException(nameof(betaService)); - } - - if (defaultMaxOutputTokens is <= 0) - { - throw new ArgumentOutOfRangeException(nameof(defaultMaxOutputTokens), "Default max tokens must be greater than zero."); - } - - return new AnthropicChatClient(betaService, defaultModelId, defaultMaxOutputTokens); - } - - /// Creates an to represent a raw . - /// The tool to wrap as an . - /// The wrapped as an . - /// - /// - /// The returned tool is only suitable for use with the returned by - /// (or s that delegate - /// to such an instance). It is likely to be ignored by any other implementation. - /// - /// - /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, - /// such as , , , or - /// , those types should be preferred instead of this method, as they are more portable, - /// capable of being respected by any implementation. This method does not attempt to - /// map the supplied to any of those types, it simply wraps it as-is: - /// the returned by will - /// be able to unwrap the when it processes the list of tools. - /// - /// - public static AITool AsAITool(this BetaToolUnion tool) - { - if (tool is null) - { - throw new ArgumentNullException(nameof(tool)); - } - - return new ToolUnionAITool(tool); - } - - private sealed class AnthropicChatClient : IChatClient - { - private const int DefaultMaxTokens = 1024; - private readonly IBetaService _betaService; - private readonly string? _defaultModelId; - private readonly int _defaultMaxTokens; - private readonly IAnthropicClient _anthropicClient; - - private static readonly AIJsonSchemaTransformCache s_transformCache = new(new AIJsonSchemaTransformOptions - { - DisallowAdditionalProperties = true, - TransformSchemaNode = (ctx, schemaNode) => - { - if (schemaNode is JsonObject schemaObj) - { - // From https://platform.claude.com/docs/en/build-with-claude/structured-outputs - ReadOnlySpan unsupportedProperties = - [ - "minimum", "maximum", "multipleOf", - "minLength", "maxLength", - ]; - - foreach (string propName in unsupportedProperties) - { - _ = schemaObj.Remove(propName); - } - } - - return schemaNode; - }, - }); - - public AnthropicChatClient( - IBetaService betaService, - string? defaultModelId, - int? defaultMaxOutputTokens) - { - this._betaService = betaService; - this._defaultModelId = defaultModelId; - this._defaultMaxTokens = defaultMaxOutputTokens ?? DefaultMaxTokens; -#pragma warning disable IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - this._anthropicClient = betaService.GetType().GetField("_client", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! - .GetValue(betaService) as IAnthropicClient ?? throw new InvalidOperationException("Unable to access Anthropic client from BetaService."); -#pragma warning restore IL2075 // 'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations. - } - - private ChatClientMetadata? _metadata; - - /// - void IDisposable.Dispose() { } - - /// - public object? GetService(System.Type serviceType, object? serviceKey = null) - { - if (serviceType is null) - { - throw new ArgumentNullException(nameof(serviceType)); - } - - if (serviceKey is not null) - { - return null; - } - - if (serviceType == typeof(ChatClientMetadata)) - { - return this._metadata ??= new("anthropic", this._anthropicClient.BaseUrl, this._defaultModelId); - } - - if (serviceType.IsInstanceOfType(this._betaService)) - { - return this._betaService; - } - - if (serviceType.IsInstanceOfType(this)) - { - return this; - } - - return null; - } - - /// - public async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } - - List messageParams = CreateMessageParams(messages, out List? systemMessages); - MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); - - var createResult = await this._betaService.Messages.Create(createParams, cancellationToken).ConfigureAwait(false); - - ChatMessage m = new(ChatRole.Assistant, [.. createResult.Content.Select(ToAIContent)]) - { - CreatedAt = DateTimeOffset.UtcNow, - MessageId = createResult.ID, - }; - - return new(m) - { - CreatedAt = m.CreatedAt, - FinishReason = ToFinishReason(createResult.StopReason), - ModelId = createResult.Model.Raw() ?? createParams.Model.Raw(), - RawRepresentation = createResult, - ResponseId = m.MessageId, - Usage = createResult.Usage is { } usage ? ToUsageDetails(usage) : null, - }; - } - - /// - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } - - List messageParams = CreateMessageParams(messages, out List? systemMessages); - MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); - - string? messageId = null; - string? modelID = null; - UsageDetails? usageDetails = null; - ChatFinishReason? finishReason = null; - Dictionary? streamingFunctions = null; - - await foreach (var createResult in this._betaService.Messages.CreateStreaming(createParams, cancellationToken).WithCancellation(cancellationToken)) - { - List contents = []; - - switch (createResult.Value) - { - case BetaRawMessageStartEvent rawMessageStart: - if (string.IsNullOrWhiteSpace(messageId)) - { - messageId = rawMessageStart.Message.ID; - } - - if (string.IsNullOrWhiteSpace(modelID)) - { - modelID = rawMessageStart.Message.Model; - } - - if (rawMessageStart.Message.Usage is { } usage) - { - UsageDetails current = ToUsageDetails(usage); - if (usageDetails is null) - { - usageDetails = current; - } - else - { - usageDetails.Add(current); - } - } - break; - - case BetaRawMessageDeltaEvent rawMessageDelta: - finishReason = ToFinishReason(rawMessageDelta.Delta.StopReason); - if (rawMessageDelta.Usage is { } deltaUsage) - { - UsageDetails current = ToUsageDetails(deltaUsage); - if (usageDetails is null) - { - usageDetails = current; - } - else - { - usageDetails.Add(current); - } - } - break; - - case BetaRawContentBlockStartEvent contentBlockStart: - switch (contentBlockStart.ContentBlock.Value) - { - case BetaTextBlock text: - contents.Add(new TextContent(text.Text) - { - RawRepresentation = text, - }); - break; - - case BetaThinkingBlock thinking: - contents.Add(new TextReasoningContent(thinking.Thinking) - { - ProtectedData = thinking.Signature, - RawRepresentation = thinking, - }); - break; - - case BetaRedactedThinkingBlock redactedThinking: - contents.Add(new TextReasoningContent(string.Empty) - { - ProtectedData = redactedThinking.Data, - RawRepresentation = redactedThinking, - }); - break; - - case BetaToolUseBlock toolUse: - streamingFunctions ??= []; - streamingFunctions[contentBlockStart.Index] = new() - { - CallId = toolUse.ID, - Name = toolUse.Name, - }; - break; - } - break; - - case BetaRawContentBlockDeltaEvent contentBlockDelta: - switch (contentBlockDelta.Delta.Value) - { - case BetaTextDelta textDelta: - contents.Add(new TextContent(textDelta.Text) - { - RawRepresentation = textDelta, - }); - break; - - case BetaInputJSONDelta inputDelta: - if (streamingFunctions is not null && - streamingFunctions.TryGetValue(contentBlockDelta.Index, out var functionData)) - { - functionData.Arguments.Append(inputDelta.PartialJSON); - } - break; - - case BetaThinkingDelta thinkingDelta: - contents.Add(new TextReasoningContent(thinkingDelta.Thinking) - { - RawRepresentation = thinkingDelta, - }); - break; - - case BetaSignatureDelta signatureDelta: - contents.Add(new TextReasoningContent(null) - { - ProtectedData = signatureDelta.Signature, - RawRepresentation = signatureDelta, - }); - break; - } - break; - - case BetaRawContentBlockStopEvent contentBlockStop: - if (streamingFunctions is not null) - { - foreach (var sf in streamingFunctions) - { - contents.Add(FunctionCallContent.CreateFromParsedArguments(sf.Value.Arguments.ToString(), sf.Value.CallId, sf.Value.Name, - json => JsonSerializer.Deserialize>(json, AnthropicClientJsonContext.Default.DictionaryStringObject))); - } - } - break; - } - - yield return new(ChatRole.Assistant, contents) - { - CreatedAt = DateTimeOffset.UtcNow, - FinishReason = finishReason, - MessageId = messageId, - RawRepresentation = createResult, - ResponseId = messageId, - ModelId = modelID, - }; - } - - if (usageDetails is not null) - { - yield return new(ChatRole.Assistant, [new UsageContent(usageDetails)]) - { - CreatedAt = DateTimeOffset.UtcNow, - FinishReason = finishReason, - MessageId = messageId, - ResponseId = messageId, - ModelId = modelID, - }; - } - } - - private static List CreateMessageParams(IEnumerable messages, out List? systemMessages) - { - List messageParams = []; - systemMessages = null; - - foreach (ChatMessage message in messages) - { - if (message.Role == ChatRole.System) - { - foreach (AIContent content in message.Contents) - { - if (content is TextContent tc) - { - (systemMessages ??= []).Add(new() { Text = tc.Text }); - } - } - - continue; - } - - List contents = []; - - foreach (AIContent content in message.Contents) - { - switch (content) - { - case AIContent ac when ac.RawRepresentation is BetaContentBlockParam rawContent: - contents.Add(rawContent); - break; - - case TextContent tc: - string text = tc.Text; - if (message.Role == ChatRole.Assistant) - { - text = text.TrimEnd(); - if (!string.IsNullOrWhiteSpace(text)) - { - contents.Add(new BetaTextBlockParam() { Text = text }); - } - } - else if (!string.IsNullOrWhiteSpace(text)) - { - contents.Add(new BetaTextBlockParam() { Text = text }); - } - break; - - case TextReasoningContent trc when !string.IsNullOrEmpty(trc.Text): - contents.Add(new BetaThinkingBlockParam() - { - Thinking = trc.Text, - Signature = trc.ProtectedData ?? string.Empty, - }); - break; - - case TextReasoningContent trc when !string.IsNullOrEmpty(trc.ProtectedData): - contents.Add(new BetaRedactedThinkingBlockParam() - { - Data = trc.ProtectedData!, - }); - break; - - case DataContent dc when dc.HasTopLevelMediaType("image"): - contents.Add(new BetaImageBlockParam() - { - Source = new(new BetaBase64ImageSource() { Data = dc.Base64Data.ToString(), MediaType = dc.MediaType }) - }); - break; - - case DataContent dc when string.Equals(dc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): - contents.Add(new BetaRequestDocumentBlock() - { - Source = new(new BetaBase64PDFSource() { Data = dc.Base64Data.ToString() }), - }); - break; - - case DataContent dc when dc.HasTopLevelMediaType("text"): - contents.Add(new BetaRequestDocumentBlock() - { - Source = new(new BetaPlainTextSource() { Data = Encoding.UTF8.GetString(dc.Data.ToArray()) }), - }); - break; - - case UriContent uc when uc.HasTopLevelMediaType("image"): - contents.Add(new BetaImageBlockParam() - { - Source = new(new BetaURLImageSource() { URL = uc.Uri.AbsoluteUri }), - }); - break; - - case UriContent uc when string.Equals(uc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): - contents.Add(new BetaRequestDocumentBlock() - { - Source = new(new BetaURLPDFSource() { URL = uc.Uri.AbsoluteUri }), - }); - break; - - case HostedFileContent fc: - contents.Add(new BetaRequestDocumentBlock() - { - Source = new(new BetaFileDocumentSource(fc.FileId)) - }); - break; - - case FunctionCallContent fcc: - contents.Add(new BetaToolUseBlockParam() - { - ID = fcc.CallId, - Name = fcc.Name, - Input = fcc.Arguments?.ToDictionary(e => e.Key, e => e.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(e.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? [], - }); - break; - - case FunctionResultContent frc: - contents.Add(new BetaToolResultBlockParam() - { - ToolUseID = frc.CallId, - IsError = frc.Exception is not null, - Content = new(JsonSerializer.Serialize(frc.Result, AnthropicClientJsonContext.Default.JsonElement)), - }); - break; - } - } - - if (contents.Count == 0) - { - continue; - } - - messageParams.Add(new() - { - Role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User, - Content = contents, - }); - } - - if (messageParams.Count == 0) - { - messageParams.Add(new() { Role = Role.User, Content = new("\u200b") }); // zero-width space - } - - return messageParams; - } - - private MessageCreateParams GetMessageCreateParams(List messages, List? systemMessages, ChatOptions? options) - { - // Get the initial MessageCreateParams, either with a raw representation provided by the options - // or with only the required properties set. - MessageCreateParams? createParams = options?.RawRepresentationFactory?.Invoke(this) as MessageCreateParams; - if (createParams is not null) - { - // Merge any messages preconfigured on the params with the ones provided to the IChatClient. - createParams = createParams with { Messages = [.. createParams.Messages, .. messages] }; - } - else - { - createParams = new MessageCreateParams() - { - MaxTokens = options?.MaxOutputTokens ?? this._defaultMaxTokens, - Messages = messages, - Model = options?.ModelId ?? this._defaultModelId ?? throw new InvalidOperationException("Model ID must be specified either in ChatOptions or as the default for the client."), - }; - } - - if (options is not null) - { - if (options.Instructions is { } instructions) - { - (systemMessages ??= []).Add(new BetaTextBlockParam() { Text = instructions }); - } - - if (createParams.OutputFormat is null && options.ResponseFormat is { } responseFormat) - { - switch (responseFormat) - { - case ChatResponseFormatJson formatJson when formatJson.Schema is not null: - JsonElement schema = s_transformCache.GetOrCreateTransformedSchema(formatJson).GetValueOrDefault(); - if (schema.TryGetProperty("properties", out JsonElement properties) && properties.ValueKind is JsonValueKind.Object && - schema.TryGetProperty("required", out JsonElement required) && required.ValueKind is JsonValueKind.Array) - { - createParams = createParams with - { - OutputFormat = new BetaJSONOutputFormat() - { - Schema = new() - { - ["type"] = JsonElement.Parse("\"object\""), - ["properties"] = properties, - ["required"] = required, - ["additionalProperties"] = JsonElement.Parse("false"), - }, - }, - }; - } - break; - } - } - - if (options.StopSequences is { Count: > 0 } stopSequences) - { - createParams = createParams.StopSequences is { } existingSequences ? - createParams with { StopSequences = [.. existingSequences, .. stopSequences] } : - createParams with { StopSequences = [.. stopSequences] }; - } - - if (createParams.Temperature is null && options.Temperature is { } temperature) - { - createParams = createParams with { Temperature = temperature }; - } - - if (createParams.TopK is null && options.TopK is { } topK) - { - createParams = createParams with { TopK = topK }; - } - - if (createParams.TopP is null && options.TopP is { } topP) - { - createParams = createParams with { TopP = topP }; - } - - if (options.Tools is { } tools) - { - List? createdTools = createParams.Tools; - List? mcpServers = createParams.MCPServers; - foreach (var tool in tools) - { - switch (tool) - { - case ToolUnionAITool raw: - (createdTools ??= []).Add(raw.Tool); - break; - - case AIFunctionDeclaration af: - Dictionary properties = []; - List required = []; - JsonElement inputSchema = af.JsonSchema; - if (inputSchema.ValueKind is JsonValueKind.Object) - { - if (inputSchema.TryGetProperty("properties", out JsonElement propsElement) && propsElement.ValueKind is JsonValueKind.Object) - { - foreach (JsonProperty p in propsElement.EnumerateObject()) - { - properties[p.Name] = p.Value; - } - } - - if (inputSchema.TryGetProperty("required", out JsonElement reqElement) && reqElement.ValueKind is JsonValueKind.Array) - { - foreach (JsonElement r in reqElement.EnumerateArray()) - { - if (r.ValueKind is JsonValueKind.String && r.GetString() is { } s && !string.IsNullOrWhiteSpace(s)) - { - required.Add(s); - } - } - } - } - - (createdTools ??= []).Add(new BetaTool() - { - Name = af.Name, - Description = af.Description, - InputSchema = new(properties) { Required = required }, - }); - break; - - case HostedWebSearchTool: - (createdTools ??= []).Add(new BetaWebSearchTool20250305()); - break; - - case HostedCodeInterpreterTool: - (createdTools ??= []).Add(new BetaCodeExecutionTool20250825()); - break; - - case HostedMcpServerTool mcp: - (mcpServers ??= []).Add(mcp.AllowedTools is { Count: > 0 } allowedTools ? - new() - { - Name = mcp.Name, - URL = mcp.ServerAddress, - ToolConfiguration = new() - { - AllowedTools = [.. allowedTools], - Enabled = true, - } - } : - new() - { - Name = mcp.Name, - URL = mcp.ServerAddress, - }); - break; - } - } - - if (createdTools?.Count > 0) - { - createParams = createParams with { Tools = createdTools }; - } - - if (mcpServers?.Count > 0) - { - createParams = createParams with { MCPServers = mcpServers }; - } - } - - if (createParams.ToolChoice is null && options.ToolMode is { } toolMode) - { - BetaToolChoice? toolChoice = - toolMode is AutoChatToolMode ? new BetaToolChoiceAuto() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : - toolMode is NoneChatToolMode ? new BetaToolChoiceNone() : - toolMode is RequiredChatToolMode ? new BetaToolChoiceAny() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : - (BetaToolChoice?)null; - if (toolChoice is not null) - { - createParams = createParams with { ToolChoice = toolChoice }; - } - } - } - - if (systemMessages is not null) - { - if (createParams.System is { } existingSystem) - { - if (existingSystem.Value is string existingMessage) - { - systemMessages.Insert(0, new BetaTextBlockParam() { Text = existingMessage }); - } - else if (existingSystem.Value is IReadOnlyList existingMessages) - { - systemMessages.InsertRange(0, existingMessages); - } - } - - createParams = createParams with { System = systemMessages }; - } - - return createParams; - } - - private static UsageDetails ToUsageDetails(BetaUsage usage) => - ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); - - private static UsageDetails ToUsageDetails(BetaMessageDeltaUsage usage) => - ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); - - private static UsageDetails ToUsageDetails(long? inputTokens, long? outputTokens, long? cacheCreationInputTokens, long? cacheReadInputTokens) - { - UsageDetails usageDetails = new() - { - InputTokenCount = inputTokens, - OutputTokenCount = outputTokens, - TotalTokenCount = (inputTokens is not null || outputTokens is not null) ? (inputTokens ?? 0) + (outputTokens ?? 0) : null, - }; - - if (cacheCreationInputTokens is not null) - { - (usageDetails.AdditionalCounts ??= [])[nameof(BetaUsage.CacheCreationInputTokens)] = cacheCreationInputTokens.Value; - } - - if (cacheReadInputTokens is not null) - { - (usageDetails.AdditionalCounts ??= [])[nameof(BetaUsage.CacheReadInputTokens)] = cacheReadInputTokens.Value; - } - - return usageDetails; - } - - private static ChatFinishReason? ToFinishReason(ApiEnum? stopReason) => - stopReason?.Value() switch - { - null => null, - BetaStopReason.Refusal => ChatFinishReason.ContentFilter, - BetaStopReason.MaxTokens => ChatFinishReason.Length, - BetaStopReason.ToolUse => ChatFinishReason.ToolCalls, - _ => ChatFinishReason.Stop, - }; - - private static AIContent ToAIContent(BetaContentBlock block) - { - static AIContent FromBetaTextBlock(BetaTextBlock text) - { - TextContent tc = new(text.Text) - { - RawRepresentation = text, - }; - - if (text.Citations is { Count: > 0 }) - { - tc.Annotations = [.. text.Citations.Select(ToAIAnnotation).OfType()]; - } - - return tc; - } - - switch (block.Value) - { - case BetaTextBlock text: - return FromBetaTextBlock(text); - - case BetaThinkingBlock thinking: - return new TextReasoningContent(thinking.Thinking) - { - ProtectedData = thinking.Signature, - RawRepresentation = thinking, - }; - - case BetaRedactedThinkingBlock redactedThinking: - return new TextReasoningContent(string.Empty) - { - ProtectedData = redactedThinking.Data, - RawRepresentation = redactedThinking, - }; - - case BetaToolUseBlock toolUse: - return new FunctionCallContent( - toolUse.ID, - toolUse.Name, - toolUse.Properties.TryGetValue("input", out JsonElement element) ? - JsonSerializer.Deserialize>(element, AnthropicClientJsonContext.Default.DictionaryStringObject) : - null) - { - RawRepresentation = toolUse, - }; - - case BetaMCPToolUseBlock mcpToolUse: - return new McpServerToolCallContent(mcpToolUse.ID, mcpToolUse.Name, mcpToolUse.ServerName) - { - Arguments = mcpToolUse.Input.ToDictionary(e => e.Key, e => (object?)e.Value), - RawRepresentation = mcpToolUse, - }; - - case BetaMCPToolResultBlock mcpToolResult: - return new McpServerToolResultContent(mcpToolResult.ToolUseID) - { - Output = - mcpToolResult.IsError ? [new ErrorContent(mcpToolResult.Content.Value?.ToString())] : - mcpToolResult.Content.Value switch - { - string s => [new TextContent(s)], - IReadOnlyList texts => texts.Select(FromBetaTextBlock).ToList(), - _ => null, - }, - RawRepresentation = mcpToolResult, - }; - - case BetaCodeExecutionToolResultBlock ce: - { - CodeInterpreterToolResultContent c = new() - { - CallId = ce.ToolUseID, - RawRepresentation = ce, - }; - - if (ce.Content.TryPickError(out var ceError)) - { - (c.Outputs ??= []).Add(new ErrorContent(null) { ErrorCode = ceError.ErrorCode.Value().ToString() }); - } - - if (ce.Content.TryPickResultBlock(out var ceOutput)) - { - if (!string.IsNullOrWhiteSpace(ceOutput.Stdout)) - { - (c.Outputs ??= []).Add(new TextContent(ceOutput.Stdout)); - } - - if (!string.IsNullOrWhiteSpace(ceOutput.Stderr) || ceOutput.ReturnCode != 0) - { - (c.Outputs ??= []).Add(new ErrorContent(ceOutput.Stderr) - { - ErrorCode = ceOutput.ReturnCode.ToString(CultureInfo.InvariantCulture) - }); - } - - if (ceOutput.Content is { Count: > 0 }) - { - foreach (var ceOutputContent in ceOutput.Content) - { - (c.Outputs ??= []).Add(new HostedFileContent(ceOutputContent.FileID)); - } - } - } - - return c; - } - - case BetaBashCodeExecutionToolResultBlock ce: - // This is the same as BetaCodeExecutionToolResultBlock but with a different type names. - // Keep both of them in sync. - { - CodeInterpreterToolResultContent c = new() - { - CallId = ce.ToolUseID, - RawRepresentation = ce, - }; - - if (ce.Content.TryPickBetaBashCodeExecutionToolResultError(out var ceError)) - { - (c.Outputs ??= []).Add(new ErrorContent(null) { ErrorCode = ceError.ErrorCode.Value().ToString() }); - } - - if (ce.Content.TryPickBetaBashCodeExecutionResultBlock(out var ceOutput)) - { - if (!string.IsNullOrWhiteSpace(ceOutput.Stdout)) - { - (c.Outputs ??= []).Add(new TextContent(ceOutput.Stdout)); - } - - if (!string.IsNullOrWhiteSpace(ceOutput.Stderr) || ceOutput.ReturnCode != 0) - { - (c.Outputs ??= []).Add(new ErrorContent(ceOutput.Stderr) - { - ErrorCode = ceOutput.ReturnCode.ToString(CultureInfo.InvariantCulture) - }); - } - - if (ceOutput.Content is { Count: > 0 }) - { - foreach (var ceOutputContent in ceOutput.Content) - { - (c.Outputs ??= []).Add(new HostedFileContent(ceOutputContent.FileID)); - } - } - } - - return c; - } - - default: - return new AIContent() - { - RawRepresentation = block.Value - }; - } - } - - private static AIAnnotation? ToAIAnnotation(BetaTextCitation citation) - { - CitationAnnotation annotation = new() - { - Title = citation.Title ?? citation.DocumentTitle, - Snippet = citation.CitedText, - FileId = citation.FileID, - }; - - if (citation.TryPickCitationsWebSearchResultLocation(out var webSearchLocation)) - { - annotation.Url = Uri.TryCreate(webSearchLocation.URL, UriKind.Absolute, out Uri? url) ? url : null; - } - else if (citation.TryPickCitationSearchResultLocation(out var searchLocation)) - { - annotation.Url = Uri.TryCreate(searchLocation.Source, UriKind.Absolute, out Uri? url) ? url : null; - } - - return annotation; - } - - private sealed class StreamingFunctionData - { - public string CallId { get; set; } = ""; - public string Name { get; set; } = ""; - public StringBuilder Arguments { get; } = new(); - } - } - - private sealed class ToolUnionAITool(BetaToolUnion tool) : AITool - { - public BetaToolUnion Tool => tool; - - public override string Name => tool.Value?.GetType().Name ?? base.Name; - - public override object? GetService(System.Type serviceType, object? serviceKey = null) => - serviceKey is null && serviceType?.IsInstanceOfType(tool) is true ? tool : - base.GetService(serviceType!, serviceKey); - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs deleted file mode 100644 index 92e60b98fd..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/External/AnthropicClientExtensions.cs +++ /dev/null @@ -1,737 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// Adapted polyfill from https://raw.githubusercontent.com/stephentoub/anthropic-sdk-csharp/3034edde7c21ac1650b3358a7812b59685eff3a9/src/Anthropic/AnthropicClientExtensions.cs -// To be deleted once PR is Merged: https://github.com/anthropics/anthropic-sdk-csharp/pull/10 - -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using Anthropic; -using Anthropic.Core; -using Anthropic.Models.Messages; -using Microsoft.Agents.AI.Anthropic; - -#pragma warning disable IDE0130 // Namespace does not match folder structure - -namespace Microsoft.Extensions.AI; - -/// -/// Provides extension methods for Anthropic clients with chat interfaces and tool representations. -/// -public static class AnthropicClientExtensions -{ - /// Gets an for use with this . - /// The client. - /// - /// The default ID of the model to use. - /// If , it must be provided per request via . - /// - /// - /// The default maximum number of tokens to generate in a response. - /// This may be overridden with . - /// If no value is provided for this parameter or in , a default maximum will be used. - /// - /// An that can be used to converse via the . - /// is . - public static IChatClient AsIChatClient( - this IAnthropicClient client, - string? defaultModelId = null, - int? defaultMaxOutputTokens = null) - { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } - - if (defaultMaxOutputTokens is <= 0) - { - throw new ArgumentOutOfRangeException(nameof(defaultMaxOutputTokens), "Default max tokens must be greater than zero."); - } - - return new AnthropicChatClient(client, defaultModelId, defaultMaxOutputTokens); - } - - /// Creates an to represent a raw . - /// The tool to wrap as an . - /// The wrapped as an . - /// - /// - /// The returned tool is only suitable for use with the returned by - /// (or s that delegate - /// to such an instance). It is likely to be ignored by any other implementation. - /// - /// - /// When a tool has a corresponding -derived type already defined in Microsoft.Extensions.AI, - /// such as , , , or - /// , those types should be preferred instead of this method, as they are more portable, - /// capable of being respected by any implementation. This method does not attempt to - /// map the supplied to any of those types, it simply wraps it as-is: - /// the returned by will - /// be able to unwrap the when it processes the list of tools. - /// - /// - public static AITool AsAITool(this ToolUnion tool) - { - if (tool is null) - { - throw new ArgumentNullException(nameof(tool)); - } - - return new ToolUnionAITool(tool); - } - - private sealed class AnthropicChatClient( - IAnthropicClient anthropicClient, - string? defaultModelId, - int? defaultMaxTokens) : IChatClient - { - private const int DefaultMaxTokens = 1024; - - private readonly IAnthropicClient _anthropicClient = anthropicClient; - private readonly string? _defaultModelId = defaultModelId; - private readonly int _defaultMaxTokens = defaultMaxTokens ?? DefaultMaxTokens; - private ChatClientMetadata? _metadata; - - /// - void IDisposable.Dispose() { } - - /// - public object? GetService(System.Type serviceType, object? serviceKey = null) - { - if (serviceType is null) - { - throw new ArgumentNullException(nameof(serviceType)); - } - - if (serviceKey is not null) - { - return null; - } - - if (serviceType == typeof(ChatClientMetadata)) - { - return this._metadata ??= new("anthropic", this._anthropicClient.BaseUrl, this._defaultModelId); - } - - if (serviceType.IsInstanceOfType(this._anthropicClient)) - { - return this._anthropicClient; - } - - if (serviceType.IsInstanceOfType(this)) - { - return this; - } - - return null; - } - - /// - public async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } - - List messageParams = CreateMessageParams(messages, out List? systemMessages); - MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); - - var createResult = await this._anthropicClient.Messages.Create(createParams, cancellationToken).ConfigureAwait(false); - - ChatMessage m = new(ChatRole.Assistant, [.. createResult.Content.Select(ToAIContent)]) - { - CreatedAt = DateTimeOffset.UtcNow, - MessageId = createResult.ID, - }; - - return new(m) - { - CreatedAt = m.CreatedAt, - FinishReason = ToFinishReason(createResult.StopReason), - ModelId = createResult.Model.Raw() ?? createParams.Model.Raw(), - RawRepresentation = createResult, - ResponseId = m.MessageId, - Usage = createResult.Usage is { } usage ? ToUsageDetails(usage) : null, - }; - } - - /// - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } - - List messageParams = CreateMessageParams(messages, out List? systemMessages); - MessageCreateParams createParams = this.GetMessageCreateParams(messageParams, systemMessages, options); - - string? messageId = null; - string? modelID = null; - UsageDetails? usageDetails = null; - ChatFinishReason? finishReason = null; - Dictionary? streamingFunctions = null; - - await foreach (var createResult in this._anthropicClient.Messages.CreateStreaming(createParams, cancellationToken).WithCancellation(cancellationToken)) - { - List contents = []; - - switch (createResult.Value) - { - case RawMessageStartEvent rawMessageStart: - if (string.IsNullOrWhiteSpace(messageId)) - { - messageId = rawMessageStart.Message.ID; - } - - if (string.IsNullOrWhiteSpace(modelID)) - { - modelID = rawMessageStart.Message.Model; - } - - if (rawMessageStart.Message.Usage is { } usage) - { - UsageDetails current = ToUsageDetails(usage); - if (usageDetails is null) - { - usageDetails = current; - } - else - { - usageDetails.Add(current); - } - } - break; - - case RawMessageDeltaEvent rawMessageDelta: - finishReason = ToFinishReason(rawMessageDelta.Delta.StopReason); - if (rawMessageDelta.Usage is { } deltaUsage) - { - UsageDetails current = ToUsageDetails(deltaUsage); - if (usageDetails is null) - { - usageDetails = current; - } - else - { - usageDetails.Add(current); - } - } - break; - - case RawContentBlockStartEvent contentBlockStart: - switch (contentBlockStart.ContentBlock.Value) - { - case TextBlock text: - contents.Add(new TextContent(text.Text) - { - RawRepresentation = text, - }); - break; - - case ThinkingBlock thinking: - contents.Add(new TextReasoningContent(thinking.Thinking) - { - ProtectedData = thinking.Signature, - RawRepresentation = thinking, - }); - break; - - case RedactedThinkingBlock redactedThinking: - contents.Add(new TextReasoningContent(string.Empty) - { - ProtectedData = redactedThinking.Data, - RawRepresentation = redactedThinking, - }); - break; - - case ToolUseBlock toolUse: - streamingFunctions ??= []; - streamingFunctions[contentBlockStart.Index] = new() - { - CallId = toolUse.ID, - Name = toolUse.Name, - }; - break; - } - break; - - case RawContentBlockDeltaEvent contentBlockDelta: - switch (contentBlockDelta.Delta.Value) - { - case TextDelta textDelta: - contents.Add(new TextContent(textDelta.Text) - { - RawRepresentation = textDelta, - }); - break; - - case InputJSONDelta inputDelta: - if (streamingFunctions is not null && - streamingFunctions.TryGetValue(contentBlockDelta.Index, out StreamingFunctionData? functionData)) - { - functionData.Arguments.Append(inputDelta.PartialJSON); - } - break; - - case ThinkingDelta thinkingDelta: - contents.Add(new TextReasoningContent(thinkingDelta.Thinking) - { - RawRepresentation = thinkingDelta, - }); - break; - - case SignatureDelta signatureDelta: - contents.Add(new TextReasoningContent(null) - { - ProtectedData = signatureDelta.Signature, - RawRepresentation = signatureDelta, - }); - break; - } - break; - - case RawContentBlockStopEvent contentBlockStop: - if (streamingFunctions is not null) - { - foreach (var sf in streamingFunctions) - { - contents.Add(FunctionCallContent.CreateFromParsedArguments(sf.Value.Arguments.ToString(), sf.Value.CallId, sf.Value.Name, - json => JsonSerializer.Deserialize>(json, AnthropicClientJsonContext.Default.DictionaryStringObject))); - } - } - break; - } - - yield return new(ChatRole.Assistant, contents) - { - CreatedAt = DateTimeOffset.UtcNow, - FinishReason = finishReason, - MessageId = messageId, - ModelId = modelID, - RawRepresentation = createResult, - ResponseId = messageId, - }; - } - - if (usageDetails is not null) - { - yield return new(ChatRole.Assistant, [new UsageContent(usageDetails)]) - { - CreatedAt = DateTimeOffset.UtcNow, - FinishReason = finishReason, - MessageId = messageId, - ModelId = modelID, - ResponseId = messageId, - }; - } - } - - private static List CreateMessageParams(IEnumerable messages, out List? systemMessages) - { - List messageParams = []; - systemMessages = null; - - foreach (ChatMessage message in messages) - { - if (message.Role == ChatRole.System) - { - foreach (AIContent content in message.Contents) - { - if (content is TextContent tc) - { - (systemMessages ??= []).Add(new() { Text = tc.Text }); - } - } - - continue; - } - - List contents = []; - - foreach (AIContent content in message.Contents) - { - switch (content) - { - case AIContent ac when ac.RawRepresentation is ContentBlockParam rawContent: - contents.Add(rawContent); - break; - - case TextContent tc: - string text = tc.Text; - if (message.Role == ChatRole.Assistant) - { - text = text.TrimEnd(); - if (!string.IsNullOrWhiteSpace(text)) - { - contents.Add(new TextBlockParam() { Text = text }); - } - } - else if (!string.IsNullOrWhiteSpace(text)) - { - contents.Add(new TextBlockParam() { Text = text }); - } - break; - - case TextReasoningContent trc when !string.IsNullOrEmpty(trc.Text): - contents.Add(new ThinkingBlockParam() - { - Thinking = trc.Text, - Signature = trc.ProtectedData ?? string.Empty, - }); - break; - - case TextReasoningContent trc when !string.IsNullOrEmpty(trc.ProtectedData): - contents.Add(new RedactedThinkingBlockParam() - { - Data = trc.ProtectedData!, - }); - break; - - case DataContent dc when dc.HasTopLevelMediaType("image"): - contents.Add(new ImageBlockParam() - { - Source = new(new Base64ImageSource() { Data = dc.Base64Data.ToString(), MediaType = dc.MediaType }) - }); - break; - - case DataContent dc when string.Equals(dc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): - contents.Add(new DocumentBlockParam() - { - Source = new(new Base64PDFSource() { Data = dc.Base64Data.ToString() }), - }); - break; - - case DataContent dc when dc.HasTopLevelMediaType("text"): - contents.Add(new DocumentBlockParam() - { - Source = new(new PlainTextSource() { Data = Encoding.UTF8.GetString(dc.Data.ToArray()) }), - }); - break; - - case UriContent uc when uc.HasTopLevelMediaType("image"): - contents.Add(new ImageBlockParam() - { - Source = new(new URLImageSource() { URL = uc.Uri.AbsoluteUri }), - }); - break; - - case UriContent uc when string.Equals(uc.MediaType, "application/pdf", StringComparison.OrdinalIgnoreCase): - contents.Add(new DocumentBlockParam() - { - Source = new(new URLPDFSource() { URL = uc.Uri.AbsoluteUri }), - }); - break; - - case FunctionCallContent fcc: - contents.Add(new ToolUseBlockParam() - { - ID = fcc.CallId, - Name = fcc.Name, - Input = fcc.Arguments?.ToDictionary(e => e.Key, e => e.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(e.Value, AnthropicClientJsonContext.Default.JsonElement)) ?? [], - }); - break; - - case FunctionResultContent frc: - contents.Add(new ToolResultBlockParam() - { - ToolUseID = frc.CallId, - IsError = frc.Exception is not null, - Content = new(JsonSerializer.Serialize(frc.Result, AnthropicClientJsonContext.Default.JsonElement)), - }); - break; - } - } - - if (contents.Count == 0) - { - continue; - } - - messageParams.Add(new() - { - Role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User, - Content = contents, - }); - } - - if (messageParams.Count == 0) - { - messageParams.Add(new() { Role = Role.User, Content = new("\u200b") }); // zero-width space - } - - return messageParams; - } - - private MessageCreateParams GetMessageCreateParams(List messages, List? systemMessages, ChatOptions? options) - { - // Get the initial MessageCreateParams, either with a raw representation provided by the options - // or with only the required properties set. - MessageCreateParams? createParams = options?.RawRepresentationFactory?.Invoke(this) as MessageCreateParams; - if (createParams is not null) - { - // Merge any messages preconfigured on the params with the ones provided to the IChatClient. - createParams = createParams with { Messages = [.. createParams.Messages, .. messages] }; - } - else - { - createParams = new MessageCreateParams() - { - MaxTokens = options?.MaxOutputTokens ?? this._defaultMaxTokens, - Messages = messages, - Model = options?.ModelId ?? this._defaultModelId ?? throw new InvalidOperationException("Model ID must be specified either in ChatOptions or as the default for the client."), - }; - } - - // Handle any other options to propagate to the create params. - if (options is not null) - { - if (options.Instructions is { } instructions) - { - (systemMessages ??= []).Add(new TextBlockParam() { Text = instructions }); - } - - if (options.StopSequences is { Count: > 0 } stopSequences) - { - createParams = createParams.StopSequences is { } existingSequences ? - createParams with { StopSequences = [.. existingSequences, .. stopSequences] } : - createParams with { StopSequences = [.. stopSequences] }; - } - - if (createParams.Temperature is null && options.Temperature is { } temperature) - { - createParams = createParams with { Temperature = temperature }; - } - - if (createParams.TopK is null && options.TopK is { } topK) - { - createParams = createParams with { TopK = topK }; - } - - if (createParams.TopP is null && options.TopP is { } topP) - { - createParams = createParams with { TopP = topP }; - } - - if (options.Tools is { } tools) - { - List? createdTools = createParams.Tools; - foreach (var tool in tools) - { - switch (tool) - { - case ToolUnionAITool raw: - (createdTools ??= []).Add(raw.Tool); - break; - - case AIFunctionDeclaration af: - Dictionary properties = []; - List required = []; - JsonElement inputSchema = af.JsonSchema; - if (inputSchema.ValueKind is JsonValueKind.Object) - { - if (inputSchema.TryGetProperty("properties", out JsonElement propsElement) && propsElement.ValueKind is JsonValueKind.Object) - { - foreach (JsonProperty p in propsElement.EnumerateObject()) - { - properties[p.Name] = p.Value; - } - } - - if (inputSchema.TryGetProperty("required", out JsonElement reqElement) && reqElement.ValueKind is JsonValueKind.Array) - { - foreach (JsonElement r in reqElement.EnumerateArray()) - { - if (r.ValueKind is JsonValueKind.String && r.GetString() is { } s && !string.IsNullOrWhiteSpace(s)) - { - required.Add(s); - } - } - } - } - - (createdTools ??= []).Add(new Tool() - { - Name = af.Name, - Description = af.Description, - InputSchema = new(properties) { Required = required }, - }); - break; - - case HostedWebSearchTool: - (createdTools ??= []).Add(new WebSearchTool20250305()); - break; - } - } - - if (createdTools?.Count > 0) - { - createParams = createParams with { Tools = createdTools }; - } - } - - if (createParams.ToolChoice is null && options.ToolMode is { } toolMode) - { - ToolChoice? toolChoice = - toolMode is AutoChatToolMode ? new ToolChoiceAuto() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : - toolMode is NoneChatToolMode ? new ToolChoiceNone() : - toolMode is RequiredChatToolMode ? new ToolChoiceAny() { DisableParallelToolUse = !options.AllowMultipleToolCalls } : - (ToolChoice?)null; - if (toolChoice is not null) - { - createParams = createParams with { ToolChoice = toolChoice }; - } - } - } - - if (systemMessages is not null) - { - if (createParams.System is { } existingSystem) - { - if (existingSystem.Value is string existingMessage) - { - systemMessages.Insert(0, new TextBlockParam() { Text = existingMessage }); - } - else if (existingSystem.Value is IReadOnlyList existingMessages) - { - systemMessages.InsertRange(0, existingMessages); - } - } - - createParams = createParams with { System = systemMessages }; - } - - return createParams; - } - - private static UsageDetails ToUsageDetails(Usage usage) => - ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); - - private static UsageDetails ToUsageDetails(MessageDeltaUsage usage) => - ToUsageDetails(usage.InputTokens, usage.OutputTokens, usage.CacheCreationInputTokens, usage.CacheReadInputTokens); - - private static UsageDetails ToUsageDetails(long? inputTokens, long? outputTokens, long? cacheCreationInputTokens, long? cacheReadInputTokens) - { - UsageDetails usageDetails = new() - { - InputTokenCount = inputTokens, - OutputTokenCount = outputTokens, - TotalTokenCount = (inputTokens is not null || outputTokens is not null) ? (inputTokens ?? 0) + (outputTokens ?? 0) : null, - }; - - if (cacheCreationInputTokens is not null) - { - (usageDetails.AdditionalCounts ??= [])[nameof(Usage.CacheCreationInputTokens)] = cacheCreationInputTokens.Value; - } - - if (cacheReadInputTokens is not null) - { - (usageDetails.AdditionalCounts ??= [])[nameof(Usage.CacheReadInputTokens)] = cacheReadInputTokens.Value; - } - - return usageDetails; - } - - private static ChatFinishReason? ToFinishReason(ApiEnum? stopReason) => - stopReason?.Value() switch - { - null => null, - StopReason.Refusal => ChatFinishReason.ContentFilter, - StopReason.MaxTokens => ChatFinishReason.Length, - StopReason.ToolUse => ChatFinishReason.ToolCalls, - _ => ChatFinishReason.Stop, - }; - - private static AIContent ToAIContent(ContentBlock block) - { - switch (block.Value) - { - case TextBlock text: - TextContent tc = new(text.Text) - { - RawRepresentation = text, - }; - - if (text.Citations is { Count: > 0 }) - { - tc.Annotations = [.. text.Citations.Select(ToAIAnnotation).OfType()]; - } - - return tc; - - case ThinkingBlock thinking: - return new TextReasoningContent(thinking.Thinking) - { - ProtectedData = thinking.Signature, - RawRepresentation = thinking, - }; - - case RedactedThinkingBlock redactedThinking: - return new TextReasoningContent(string.Empty) - { - ProtectedData = redactedThinking.Data, - RawRepresentation = redactedThinking, - }; - - case ToolUseBlock toolUse: - return new FunctionCallContent( - toolUse.ID, - toolUse.Name, - toolUse.Properties.TryGetValue("input", out JsonElement element) ? - JsonSerializer.Deserialize>(element, AnthropicClientJsonContext.Default.DictionaryStringObject) : - null) - { - RawRepresentation = toolUse, - }; - - default: - return new AIContent() - { - RawRepresentation = block.Value, - }; - } - } - - private static AIAnnotation? ToAIAnnotation(TextCitation citation) - { - CitationAnnotation annotation = new() - { - Title = citation.Title ?? citation.DocumentTitle, - Snippet = citation.CitedText, - FileId = citation.FileID, - }; - - if (citation.TryPickCitationsWebSearchResultLocation(out var webSearchLocation)) - { - annotation.Url = Uri.TryCreate(webSearchLocation.URL, UriKind.Absolute, out Uri? url) ? url : null; - } - else if (citation.TryPickCitationsSearchResultLocation(out var searchLocation)) - { - annotation.Url = Uri.TryCreate(searchLocation.Source, UriKind.Absolute, out Uri? url) ? url : null; - } - - return annotation; - } - - private sealed class StreamingFunctionData - { - public string CallId { get; set; } = ""; - public string Name { get; set; } = ""; - public StringBuilder Arguments { get; } = new(); - } - } - - private sealed class ToolUnionAITool(ToolUnion tool) : AITool - { - public ToolUnion Tool => tool; - - public override string Name => tool.Value?.GetType().Name ?? base.Name; - - public override object? GetService(System.Type serviceType, object? serviceKey = null) => - serviceKey is null && serviceType?.IsInstanceOfType(tool) is true ? tool : - base.GetService(serviceType!, serviceKey); - } -} diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj index 6c4ab3142a..dccbff2834 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj @@ -1,7 +1,8 @@ - net9.0 + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) True From 3096b276c1c1d6bdf4a534cdd59d1664b8be1322 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 21 Nov 2025 15:19:41 +0000 Subject: [PATCH 14/24] Skipping Anthropic IT's for manual execution and avoid pipeline execution --- ...pletionChatClientAgentRunStreamingTests.cs | 25 +++++++++++------ ...icChatCompletionChatClientAgentRunTests.cs | 21 +++++++++++--- ...nthropicChatCompletionRunStreamingTests.cs | 28 ++++++++++++++++--- .../AnthropicChatCompletionRunTests.cs | 28 ++++++++++++++++--- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs index fd05014471..d6b9a76860 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -1,17 +1,26 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Threading.Tasks; using AgentConformance.IntegrationTests; namespace AnthropicChatCompletion.IntegrationTests; -public class AnthropicBetaChatCompletionChatClientAgentRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); +public abstract class SkipAllChatClientRunStreaming(Func func) : ChatClientAgentRunStreamingTests(func) +{ + [Fact(Skip = "For manual verification.")] + public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() + => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); -public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); + [Fact(Skip = "For manual verification.")] + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); +} -public class AnthropicChatCompletionChatClientAgentRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); +public class AnthropicBetaChatCompletionChatClientAgentReasoningRunStreamingTests() : SkipAllChatClientRunStreaming(() => new(useReasoningChatModel: true, useBeta: true)); -public class AnthropicChatCompletionChatClientAgentReasoningRunStreamingTests() - : ChatClientAgentRunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); +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 index db150a2605..51db675c70 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -1,17 +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 = "For manual verification.")] + public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() + => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() + => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); +} + public class AnthropicBetaChatCompletionChatClientAgentRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: true)); + : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionChatClientAgentReasoningRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: true)); + : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionChatClientAgentRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: false, useBeta: false)); + : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionChatClientAgentReasoningRunTests() - : ChatClientAgentRunTests(() => new(useReasoningChatModel: true, useBeta: false)); + : SkipAllChatClientAgentRun(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs index ee39281ba6..74afbb2755 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -1,17 +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 = "For manual verification.")] + public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync(); +} + public class AnthropicBetaChatCompletionRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: true)); + : SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionReasoningRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: true)); + : SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: false, useBeta: false)); + : SkipAllRunStreaming(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunStreamingTests() - : RunStreamingTests(() => new(useReasoningChatModel: true, useBeta: false)); + : SkipAllRunStreaming(() => new(useReasoningChatModel: true, useBeta: false)); diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs index 6cf514e695..9acc766e90 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -1,17 +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 = "For manual verification.")] + public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); + + [Fact(Skip = "For manual verification.")] + public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync(); +} + public class AnthropicBetaChatCompletionRunTests() - : RunTests(() => new(useReasoningChatModel: false, useBeta: true)); + : SkipAllRun(() => new(useReasoningChatModel: false, useBeta: true)); public class AnthropicBetaChatCompletionReasoningRunTests() - : RunTests(() => new(useReasoningChatModel: true, useBeta: true)); + : SkipAllRun(() => new(useReasoningChatModel: true, useBeta: true)); public class AnthropicChatCompletionRunTests() - : RunTests(() => new(useReasoningChatModel: false, useBeta: false)); + : SkipAllRun(() => new(useReasoningChatModel: false, useBeta: false)); public class AnthropicChatCompletionReasoningRunTests() - : RunTests(() => new(useReasoningChatModel: true, useBeta: false)); + : SkipAllRun(() => new(useReasoningChatModel: true, useBeta: false)); From ad5b54b1a526cabb76020ff67dc83f9532214755 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:33:31 +0000 Subject: [PATCH 15/24] Fix compilation error --- .../Microsoft.Agents.AI.Anthropic.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) 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 index 7344b8bf7f..6f1426240b 100644 --- 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 @@ -2,6 +2,7 @@ $(ProjectsTargetFrameworks) + true From e7b7a4105a62ac8427b4c5f1f3ef5a1d819a3ccf Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:02:08 +0000 Subject: [PATCH 16/24] Address error in UT --- .../Extensions/AnthropicBetaServiceExtensionsTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index f0348a358d..687aadcdb0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -12,6 +12,9 @@ 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; @@ -249,6 +252,8 @@ public TestAnthropicChatClient() public IBetaService BetaService { get; } + IMessageService IAnthropicClient.Messages => new Mock().Object; + public Task Execute(HttpRequest request, CancellationToken cancellationToken = default) where T : ParamsBase { throw new NotImplementedException(); @@ -270,12 +275,12 @@ public TestBetaService(IAnthropicClient client) public global::Anthropic.Services.Beta.IModelService Models => throw new NotImplementedException(); - public global::Anthropic.Services.Beta.IMessageService Messages => 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(); From 4d9e3eb10e6e97b530b52289bd903355418cf7fc Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:22:57 +0000 Subject: [PATCH 17/24] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AgentProviders/Agent_With_Anthropic/Program.cs | 2 +- .../Agent_With_OpenAIChatCompletion/Program.cs | 6 +----- .../Agent_Anthropic_Step01_Running/Program.cs | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index 2d7f71898c..daa2422a71 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -2,7 +2,7 @@ #pragma warning disable CA1050 // Declare types in namespaces -// This sample shows how to create and use a AI agents with Azure Foundry Agents as the backend. +// This sample shows how to create and use an AI agent with Anthropic as the backend. using Anthropic; using Anthropic.Foundry; diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs index 05caf37c62..a02136dea9 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs @@ -18,8 +18,4 @@ // Invoke the agent and output the text result. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); -var responseCreationOptions = new ResponseCreationOptions(); -#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. -responseCreationOptions.Patch.Set("$.prompt_cache_key"u8, BinaryData.FromString("custom_key")); -responseCreationOptions.Patch.Set("$.prompt_cache_retention"u8, BinaryData.FromString("24h")); -#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs index 27242c2701..cf7e29c2fe 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/Program.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -// This sample shows how to create and use a simple AI agent with OpenAI as the backend. +// This sample shows how to create and use a simple AI agent with Anthropic as the backend. using Anthropic; using Anthropic.Core; From cb7c83c33e1202676545ddc182af6c6c6fe59c40 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:45:34 +0000 Subject: [PATCH 18/24] Fix warning --- .../AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs index a02136dea9..331109fba9 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_OpenAIChatCompletion/Program.cs @@ -5,7 +5,6 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI; -using OpenAI.Responses; var apiKey = Environment.GetEnvironmentVariable("OPENAI_APIKEY") ?? throw new InvalidOperationException("OPENAI_APIKEY is not set."); var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-4o-mini"; @@ -17,5 +16,3 @@ // Invoke the agent and output the text result. Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); - - From b1ed0e43358fb3856fd6c20a4dffbbe737607ba3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:47:15 +0000 Subject: [PATCH 19/24] Net 10 update --- .../Agent_With_Anthropic/Agent_With_Anthropic.csproj | 2 +- .../Agent_With_OpenAIChatCompletion.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 ccc96d4fb1..dab0d240b5 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 @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable 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 From 18b69bb54b36ea0c87c975d4c55fcc11162ec0db Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:01:46 +0000 Subject: [PATCH 20/24] Update for NET 10, remove Anthropic.Foundry due to vulnerability --- .../Agent_With_Anthropic.csproj | 3 +- .../Agent_With_Anthropic/Program.cs | 126 +++++++++++++++--- .../Agent_Anthropic_Step01_Running.csproj | 2 +- .../Microsoft.Agents.AI.Anthropic.csproj | 3 +- 4 files changed, 110 insertions(+), 24 deletions(-) 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 dab0d240b5..002bd066fe 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 @@ -2,7 +2,7 @@ Exe - net10.0 + net10.0 enable enable @@ -11,7 +11,6 @@ - diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index daa2422a71..d5ddb0ffeb 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -#pragma warning disable CA1050 // Declare types in namespaces - // This sample shows how to create and use an AI agent with Anthropic as the backend. +using System.ClientModel; using Anthropic; -using Anthropic.Foundry; +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"; @@ -23,33 +23,121 @@ 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(new AnthropicFoundryApiKeyCredentials(apiKey, resource)) // If an apiKey are provided, use Foundry with ApiKey authentication - : new AnthropicFoundryClient(new AzureTokenCredential(resource, new AzureCliCredential())); // Otherwise, use Foundry with Azure Client authentication + ? new AnthropicFoundryClient(resource, new ApiKeyCredential(apiKey)) // If an apiKey are 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.")); -public class AzureTokenCredential : IAnthropicFoundryCredentials -#pragma warning restore CA1050 // Declare types in namespaces +namespace Sample { - private readonly TokenCredential _tokenCredential; - - public AzureTokenCredential(string resourceName, TokenCredential tokenCredential) + public class AzureTokenCredential +#pragma warning restore CA1050 // Declare types in namespaces { - this.ResourceName = resourceName; - this._tokenCredential = tokenCredential; - } + private readonly TokenCredential _tokenCredential; + + public AzureTokenCredential(ApiKeyCredential apiKeyCredential) + { + this._tokenCredential = DelegatedTokenCredential.Create((_, _) => + { + apiKeyCredential.Deconstruct(out string dangerousCredential); + return new AccessToken(dangerousCredential, DateTimeOffset.UtcNow.AddMinutes(30)); + }); + } - public string ResourceName { get; } + public AzureTokenCredential(TokenCredential tokenCredential) + { + this._tokenCredential = tokenCredential; + } + + public AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return this._tokenCredential.GetToken(requestContext, cancellationToken); + } + } - public void Apply(HttpRequestMessage requestMessage) + /// + /// Provides methods for invoking the Azure hosted Anthropic api. + /// + public class AnthropicFoundryClient : AnthropicClient +#pragma warning restore CA1050 // Declare types in namespaces { - var accessToken = this._tokenCredential.GetToken( - new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), - cancellationToken: CancellationToken.None); + 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) + { } + + [Obsolete("The {nameof(APIKey)} property is not supported in this configuration.", true)] +#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member + public override string? APIKey +#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member + { + get => + throw new NotSupportedException( + $"The {nameof(this.APIKey)} property is not supported in this configuration." + ); + init => + throw new NotSupportedException( + $"The {nameof(this.APIKey)} property is not supported in this configuration." + ); + } + + public override IAnthropicClient WithOptions(Func modifier) + { + return new AnthropicFoundryClient(this._resourceName, this._tokenCredential, modifier(this._options)); + } + + 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 System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken.Token); - requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken.Token); + return default; + } } } 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 index e1d991ce96..09359c5e78 100644 --- 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 @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable 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 index 63211ce76e..8040d41a8d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -1,9 +1,8 @@  - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) preview + $(TargetFrameworksCore);netstandard2.0;net472 enable true From c9170e4a5a4107558ca3c3e755bceed752721ea8 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:13:50 +0000 Subject: [PATCH 21/24] Final missing adjustments for NET 10 --- .../Agent_Anthropic_Step02_Reasoning.csproj | 2 +- .../Agent_Anthropic_Step03_UsingFunctionTools.csproj | 2 +- .../Microsoft.Agents.AI.Anthropic.csproj | 1 - .../AnthropicChatCompletion.IntegrationTests.csproj | 3 --- .../Microsoft.Agents.AI.Anthropic.UnitTests.csproj | 1 - 5 files changed, 2 insertions(+), 7 deletions(-) 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 index 949c60146e..fc0914f1fc 100644 --- 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 @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable 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 index 9c89136316..fdb9a2f50f 100644 --- 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 @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable 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 index 8040d41a8d..6ab17da409 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -2,7 +2,6 @@ preview - $(TargetFrameworksCore);netstandard2.0;net472 enable true diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj index dccbff2834..929eafe998 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletion.IntegrationTests.csproj @@ -1,8 +1,6 @@ - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) True @@ -12,7 +10,6 @@ - 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 index 6f1426240b..291c56f879 100644 --- 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 @@ -1,7 +1,6 @@ - $(ProjectsTargetFrameworks) true From 279760471cbe2558dfc91053b1ddd6ef5b86c5de Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:39:41 +0000 Subject: [PATCH 22/24] Address PR comments --- .../Agent_With_Anthropic/Program.cs | 26 +++---------------- .../Agent_Anthropic_Step01_Running/README.md | 3 ++- .../README.md | 3 ++- .../README.md | 3 ++- .../AnthropicBetaServiceExtensions.cs | 14 +++++----- .../AnthropicClientExtensions.cs | 8 +++--- .../Microsoft.Agents.AI.Anthropic.csproj | 2 +- .../AnthropicBetaServiceExtensionsTests.cs | 2 +- .../AnthropicClientExtensionsTests.cs | 2 +- 9 files changed, 24 insertions(+), 39 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index d5ddb0ffeb..08066d4a29 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -3,6 +3,7 @@ // 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; @@ -23,7 +24,7 @@ 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 are provided, use Foundry with ApiKey authentication + ? 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); @@ -34,7 +35,6 @@ namespace Sample { public class AzureTokenCredential -#pragma warning restore CA1050 // Declare types in namespaces { private readonly TokenCredential _tokenCredential; @@ -62,7 +62,6 @@ public AccessToken GetToken(TokenRequestContext requestContext, CancellationToke /// Provides methods for invoking the Azure hosted Anthropic api. /// public class AnthropicFoundryClient : AnthropicClient -#pragma warning restore CA1050 // Declare types in namespaces { private readonly TokenCredential _tokenCredential; private readonly string _resourceName; @@ -107,25 +106,8 @@ public AnthropicFoundryClient(string resourceName, ApiKeyCredential apiKeyCreden options) { } - [Obsolete("The {nameof(APIKey)} property is not supported in this configuration.", true)] -#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member - public override string? APIKey -#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member - { - get => - throw new NotSupportedException( - $"The {nameof(this.APIKey)} property is not supported in this configuration." - ); - init => - throw new NotSupportedException( - $"The {nameof(this.APIKey)} property is not supported in this configuration." - ); - } - public override IAnthropicClient WithOptions(Func modifier) - { - return new AnthropicFoundryClient(this._resourceName, this._tokenCredential, modifier(this._options)); - } + => this; protected override ValueTask BeforeSend( HttpRequest request, @@ -135,7 +117,7 @@ CancellationToken cancellationToken { var accessToken = this._tokenCredential.GetToken(new TokenRequestContext(scopes: ["https://ai.azure.com/.default"]), cancellationToken); - requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken.Token); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token); return default; } diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md index a10b03bb5b..4800650bd9 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step01_Running/README.md @@ -21,6 +21,7 @@ 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 @@ -28,7 +29,7 @@ $env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic A Navigate to the AgentWithAnthropic sample directory and run: ```powershell -cd dotnet/samples/GettingStarted/AgentWithAnthropic +cd dotnet\samples\GettingStarted\AgentWithAnthropic dotnet run --project .\Agent_Anthropic_Step01_Running ``` diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md index f01ce88b30..ae088b2386 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step02_Reasoning/README.md @@ -23,6 +23,7 @@ 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 @@ -30,7 +31,7 @@ $env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic A Navigate to the AgentWithAnthropic sample directory and run: ```powershell -cd dotnet/samples/GettingStarted/AgentWithAnthropic +cd dotnet\samples\GettingStarted\AgentWithAnthropic dotnet run --project .\Agent_Anthropic_Step02_Reasoning ``` diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md index abe2496293..6c905864ef 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/Agent_Anthropic_Step03_UsingFunctionTools/README.md @@ -23,6 +23,7 @@ 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 @@ -30,7 +31,7 @@ $env:ANTHROPIC_API_KEY="your-anthropic-api-key" # Replace with your Anthropic A Navigate to the AgentWithAnthropic sample directory and run: ```powershell -cd dotnet/samples/GettingStarted/AgentWithAnthropic +cd dotnet\samples\GettingStarted\AgentWithAnthropic dotnet run --project .\Agent_Anthropic_Step03_UsingFunctionTools ``` diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs index a61b332c33..9bf698bded 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicBetaServiceExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -using Anthropic.Services; +using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.AI; +namespace Anthropic.Services; /// /// Provides extension methods for the class. @@ -68,24 +68,24 @@ public static ChatClientAgent CreateAIAgent( /// /// Creates an AI agent from an using the Anthropic Chat Completion API. /// - /// The Anthropic to use for the agent. + /// 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 . + /// Thrown when or is . public static ChatClientAgent CreateAIAgent( - this IBetaService client, + this IBetaService betaService, ChatClientAgentOptions options, Func? clientFactory = null, ILoggerFactory? loggerFactory = null, IServiceProvider? services = null) { - Throw.IfNull(client); + Throw.IfNull(betaService); Throw.IfNull(options); - var chatClient = client.AsIChatClient(); + var chatClient = betaService.AsIChatClient(); if (clientFactory is not null) { diff --git a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs index dc1338d8cf..f43f4bd0ce 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/AnthropicClientExtensions.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. -using Anthropic; +using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; -namespace Microsoft.Agents.AI; +namespace Anthropic; /// /// Provides extension methods for the class. @@ -20,7 +20,7 @@ public static class AnthropicClientExtensions /// /// Creates a new AI agent using the specified model and options. /// - /// The Anthropic client. + /// 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. @@ -68,7 +68,7 @@ public static ChatClientAgent CreateAIAgent( /// /// Creates an AI agent from an using the Anthropic Chat Completion API. /// - /// The Anthropic to use for the agent. + /// 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. 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 index 6ab17da409..60b90a0212 100644 --- a/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Anthropic/Microsoft.Agents.AI.Anthropic.csproj @@ -19,7 +19,7 @@ - Microsoft Agent Framework for Anthropic Agents + Microsoft Agent Framework Anthropic Agents Provides Microsoft Agent Framework support for Anthropic Agents. diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index 687aadcdb0..af778c03c9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions; /// -/// Unit tests for the class. +/// Unit tests for the AnthropicClientExtensions class. /// public sealed class AnthropicBetaServiceExtensionsTests { diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs index 37b1bd03dd..7a9c34a508 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.Agents.AI.Anthropic.UnitTests.Extensions; /// -/// Unit tests for the class. +/// Unit tests for the AnthropicClientExtensions class. /// public sealed class AnthropicClientExtensionsTests { From 9c4fc4697d7f0df17a2a5122411ab165111a3111 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:22:02 +0000 Subject: [PATCH 23/24] Remove unused code --- .../Agent_With_Anthropic/Program.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs index 08066d4a29..531b56e1a1 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_Anthropic/Program.cs @@ -34,30 +34,6 @@ namespace Sample { - public class AzureTokenCredential - { - private readonly TokenCredential _tokenCredential; - - public AzureTokenCredential(ApiKeyCredential apiKeyCredential) - { - this._tokenCredential = DelegatedTokenCredential.Create((_, _) => - { - apiKeyCredential.Deconstruct(out string dangerousCredential); - return new AccessToken(dangerousCredential, DateTimeOffset.UtcNow.AddMinutes(30)); - }); - } - - public AzureTokenCredential(TokenCredential tokenCredential) - { - this._tokenCredential = tokenCredential; - } - - public AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - return this._tokenCredential.GetToken(requestContext, cancellationToken); - } - } - /// /// Provides methods for invoking the Azure hosted Anthropic api. /// From 257ca61b01aec13aceefedfcf3c8466ef31b05ca Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:35:37 +0000 Subject: [PATCH 24/24] Address feedback --- dotnet/agent-framework-dotnet.slnx | 1 + .../GettingStarted/AgentWithAnthropic/README.md | 4 ++++ ...icChatCompletionChatClientAgentRunStreamingTests.cs | 4 ++-- .../AnthropicChatCompletionChatClientAgentRunTests.cs | 4 ++-- .../AnthropicChatCompletionFixture.cs | 3 +++ .../AnthropicChatCompletionRunStreamingTests.cs | 10 +++++----- .../AnthropicChatCompletionRunTests.cs | 10 +++++----- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 82f730981c..4f4493fc47 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -83,6 +83,7 @@ + diff --git a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md index fc1b73141c..44c15b384b 100644 --- a/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md +++ b/dotnet/samples/GettingStarted/AgentWithAnthropic/README.md @@ -18,6 +18,10 @@ Before you begin, ensure you have the following prerequisites: **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| diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs index d6b9a76860..992db5380b 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunStreamingTests.cs @@ -8,11 +8,11 @@ namespace AnthropicChatCompletion.IntegrationTests; public abstract class SkipAllChatClientRunStreaming(Func func) : ChatClientAgentRunStreamingTests(func) { - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); } diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs index 51db675c70..e2ce6e5d04 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionChatClientAgentRunTests.cs @@ -8,11 +8,11 @@ namespace AnthropicChatCompletion.IntegrationTests; public abstract class SkipAllChatClientAgentRun(Func func) : ChatClientAgentRunTests(func) { - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync() => base.RunWithFunctionsInvokesFunctionsAndReturnsExpectedResultsAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithInstructionsAndNoMessageReturnsExpectedResultAsync() => base.RunWithInstructionsAndNoMessageReturnsExpectedResultAsync(); } diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs index 78f0189f85..76ca18d3de 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionFixture.cs @@ -16,6 +16,9 @@ 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; diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs index 74afbb2755..f1bbbe47e9 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunStreamingTests.cs @@ -8,19 +8,19 @@ namespace AnthropicChatCompletion.IntegrationTests; public abstract class SkipAllRunStreaming(Func func) : RunStreamingTests(func) { - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync(); } diff --git a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs index 9acc766e90..aadbf747c2 100644 --- a/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs +++ b/dotnet/tests/AnthropicChatCompletion.IntegrationTests/AnthropicChatCompletionRunTests.cs @@ -8,19 +8,19 @@ namespace AnthropicChatCompletion.IntegrationTests; public abstract class SkipAllRun(Func func) : RunTests(func) { - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithChatMessageReturnsExpectedResultAsync() => base.RunWithChatMessageReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithNoMessageDoesNotFailAsync() => base.RunWithNoMessageDoesNotFailAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithChatMessagesReturnsExpectedResultAsync() => base.RunWithChatMessagesReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task RunWithStringReturnsExpectedResultAsync() => base.RunWithStringReturnsExpectedResultAsync(); - [Fact(Skip = "For manual verification.")] + [Fact(Skip = AnthropicChatCompletionFixture.SkipReason)] public override Task ThreadMaintainsHistoryAsync() => base.ThreadMaintainsHistoryAsync(); }