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