diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln
index 5b26e27165b3..113988a23cab 100644
--- a/dotnet/AutoGen.sln
+++ b/dotnet/AutoGen.sln
@@ -130,6 +130,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloAgentState", "samples\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Extensions.Aspire", "src\Microsoft.AutoGen\Extensions\Aspire\Microsoft.AutoGen.Extensions.Aspire.csproj", "{65059914-5527-4A00-9308-9FAF23D5E85A}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Extensions.Anthropic", "src\Microsoft.AutoGen\Extensions\Anthropic\Microsoft.AutoGen.Extensions.Anthropic.csproj", "{5ED47D4C-19D7-4684-B6F8-A4AA77F37E21}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AutoGen.Agents.Tests", "test\Microsoft.AutoGen.Agents.Tests\Microsoft.AutoGen.Agents.Tests.csproj", "{394FDAF8-74F9-4977-94A5-3371737EB774}"
EndProject
Global
@@ -338,6 +340,10 @@ Global
{65059914-5527-4A00-9308-9FAF23D5E85A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{65059914-5527-4A00-9308-9FAF23D5E85A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{65059914-5527-4A00-9308-9FAF23D5E85A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5ED47D4C-19D7-4684-B6F8-A4AA77F37E21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5ED47D4C-19D7-4684-B6F8-A4AA77F37E21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5ED47D4C-19D7-4684-B6F8-A4AA77F37E21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5ED47D4C-19D7-4684-B6F8-A4AA77F37E21}.Release|Any CPU.Build.0 = Release|Any CPU
{394FDAF8-74F9-4977-94A5-3371737EB774}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{394FDAF8-74F9-4977-94A5-3371737EB774}.Debug|Any CPU.Build.0 = Debug|Any CPU
{394FDAF8-74F9-4977-94A5-3371737EB774}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -401,6 +407,7 @@ Global
{97550E87-48C6-4EBF-85E1-413ABAE9DBFD} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
{64EF61E7-00A6-4E5E-9808-62E10993A0E5} = {7EB336C2-7C0A-4BC8-80C6-A3173AB8DC45}
{65059914-5527-4A00-9308-9FAF23D5E85A} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
+ {5ED47D4C-19D7-4684-B6F8-A4AA77F37E21} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
{394FDAF8-74F9-4977-94A5-3371737EB774} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs
index 6abc2786b0ff..114bd8c93a4f 100644
--- a/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs
+++ b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs
@@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using AutoGen.Anthropic.DTO;
+
using AutoGen.Core;
namespace AutoGen.Anthropic;
diff --git a/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj
index a4fd32e7e345..dab9b978e163 100644
--- a/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj
+++ b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj
@@ -17,6 +17,7 @@
+
diff --git a/dotnet/src/AutoGen.Anthropic/DTO/Content.cs b/dotnet/src/AutoGen.Anthropic/DTO/Content.cs
deleted file mode 100644
index 8256aacbc678..000000000000
--- a/dotnet/src/AutoGen.Anthropic/DTO/Content.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Content.cs
-
-using System.Text.Json.Nodes;
-using System.Text.Json.Serialization;
-using AutoGen.Anthropic.Converters;
-
-namespace AutoGen.Anthropic.DTO;
-
-public abstract class ContentBase
-{
- [JsonPropertyName("type")]
- public abstract string Type { get; }
-
- [JsonPropertyName("cache_control")]
- public CacheControl? CacheControl { get; set; }
-}
-
-public class TextContent : ContentBase
-{
- [JsonPropertyName("type")]
- public override string Type => "text";
-
- [JsonPropertyName("text")]
- public string? Text { get; set; }
-
- public static TextContent CreateTextWithCacheControl(string text) => new()
- {
- Text = text,
- CacheControl = new CacheControl { Type = CacheControlType.Ephemeral }
- };
-}
-
-public class ImageContent : ContentBase
-{
- [JsonPropertyName("type")]
- public override string Type => "image";
-
- [JsonPropertyName("source")]
- public ImageSource? Source { get; set; }
-}
-
-public class ImageSource
-{
- [JsonPropertyName("type")]
- public string Type => "base64";
-
- [JsonPropertyName("media_type")]
- public string? MediaType { get; set; }
-
- [JsonPropertyName("data")]
- public string? Data { get; set; }
-}
-
-public class ToolUseContent : ContentBase
-{
- [JsonPropertyName("type")]
- public override string Type => "tool_use";
-
- [JsonPropertyName("id")]
- public string? Id { get; set; }
-
- [JsonPropertyName("name")]
- public string? Name { get; set; }
-
- [JsonPropertyName("input")]
- public JsonNode? Input { get; set; }
-}
-
-public class ToolResultContent : ContentBase
-{
- [JsonPropertyName("type")]
- public override string Type => "tool_result";
-
- [JsonPropertyName("tool_use_id")]
- public string? Id { get; set; }
-
- [JsonPropertyName("content")]
- public string? Content { get; set; }
-}
-
-public class CacheControl
-{
- [JsonPropertyName("type")]
- public CacheControlType Type { get; set; }
-
- public static CacheControl Create() => new CacheControl { Type = CacheControlType.Ephemeral };
-}
-
-[JsonConverter(typeof(JsonPropertyNameEnumConverter))]
-public enum CacheControlType
-{
- [JsonPropertyName("ephemeral")]
- Ephemeral
-}
diff --git a/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs b/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs
deleted file mode 100644
index e230899f22a9..000000000000
--- a/dotnet/src/AutoGen.Anthropic/DTO/Tool.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Tool.cs
-
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
-namespace AutoGen.Anthropic.DTO;
-
-public class Tool
-{
- [JsonPropertyName("name")]
- public string? Name { get; set; }
-
- [JsonPropertyName("description")]
- public string? Description { get; set; }
-
- [JsonPropertyName("input_schema")]
- public InputSchema? InputSchema { get; set; }
-
- [JsonPropertyName("cache_control")]
- public CacheControl? CacheControl { get; set; }
-}
-
-public class InputSchema
-{
- [JsonPropertyName("type")]
- public string? Type { get; set; }
-
- [JsonPropertyName("properties")]
- public Dictionary? Properties { get; set; }
-
- [JsonPropertyName("required")]
- public List? Required { get; set; }
-}
-
-public class SchemaProperty
-{
- [JsonPropertyName("type")]
- public string? Type { get; set; }
-
- [JsonPropertyName("description")]
- public string? Description { get; set; }
-}
diff --git a/dotnet/src/AutoGen.Anthropic/TypeForwarding.cs b/dotnet/src/AutoGen.Anthropic/TypeForwarding.cs
new file mode 100644
index 000000000000..0fea360c92c3
--- /dev/null
+++ b/dotnet/src/AutoGen.Anthropic/TypeForwarding.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// TypeForwarding.cs
+
+using System.Runtime.CompilerServices;
+
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.Converters.ContentBaseConverter))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.Converters.SystemMessageConverter))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.AIContentExtensions))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.CacheControl))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.CacheControlType))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ChatCompletionRequest))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ChatCompletionResponse))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ChatMessage))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ContentBase))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.Delta))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.Error))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ErrorResponse))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ImageContent))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ImageSource))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.InputSchema))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.SchemaProperty))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.StreamingMessage))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.SystemMessage))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.TextContent))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.Tool))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ToolChoice))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ToolChoiceType))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ToolResultContent))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.ToolUseContent))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.DTO.Usage))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.Utils.AnthropicConstants))]
+[assembly: TypeForwardedTo(typeof(AutoGen.Anthropic.AnthropicClient))]
diff --git a/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs b/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs
deleted file mode 100644
index e445b42ee109..000000000000
--- a/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// AnthropicConstants.cs
-
-namespace AutoGen.Anthropic.Utils;
-
-public static class AnthropicConstants
-{
- public static string Endpoint = "https://api.anthropic.com/v1/messages";
-
- // Models
- public static string Claude3Opus = "claude-3-opus-20240229";
- public static string Claude3Sonnet = "claude-3-sonnet-20240229";
- public static string Claude3Haiku = "claude-3-haiku-20240307";
- public static string Claude35Sonnet = "claude-3-5-sonnet-20240620";
-}
diff --git a/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicChatCompletionClient.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicChatCompletionClient.cs
new file mode 100644
index 000000000000..9464874a9e1f
--- /dev/null
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicChatCompletionClient.cs
@@ -0,0 +1,290 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// AnthropicChatCompletionClient.cs
+
+using System.Runtime.CompilerServices;
+using AutoGen.Anthropic.DTO;
+using Microsoft.Extensions.AI;
+using MEAI = Microsoft.Extensions.AI;
+using DTO = AutoGen.Anthropic.DTO;
+
+using AutoGen.Anthropic;
+
+#if NET8_0_OR_GREATER
+using System.Diagnostics.CodeAnalysis;
+#endif
+
+namespace Microsoft.AutoGen.Extensions.Anthropic;
+
+public static class AnthropicChatCompletionDefaults
+{
+ //public const string DefaultSystemMessage = "You are a helpful AI assistant";
+ public const decimal DefaultTemperature = 0.7m;
+ public const int DefaultMaxTokens = 1024;
+}
+
+public sealed class AnthropicChatCompletionClient : IChatClient, IDisposable
+{
+ private AnthropicClient? _anthropicClient;
+ private string _modelId;
+
+ public AnthropicChatCompletionClient(HttpClient httpClient, string modelId, string baseUrl, string apiKey)
+ : this(new AnthropicClient(httpClient, baseUrl, apiKey), modelId)
+ {
+ }
+
+ public AnthropicChatCompletionClient(
+#if NET8_0_OR_GREATER // TODO: Should this be lower?
+ [NotNull]
+#endif
+ AnthropicClient client, string modelId)
+ {
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ _anthropicClient = client;
+ _modelId = modelId;
+
+ if (!Uri.TryCreate(client.BaseUrl, UriKind.Absolute, out Uri? uri))
+ {
+ // technically we should never be able to get this far, in this case
+ throw new ArgumentException($"Invalid base URL in provided client: {client.BaseUrl}", nameof(client));
+ }
+
+ this.Metadata = new ChatClientMetadata("Anthropic", uri, modelId);
+ }
+
+ public ChatClientMetadata Metadata { get; private set; }
+
+ private DTO.ChatMessage Translate(MEAI.ChatMessage message, List? systemMessagesSink = null)
+ {
+ if (message.Role == ChatRole.System && systemMessagesSink != null)
+ {
+ if (message.Contents.Count != 1 || message.Text == null)
+ {
+ throw new Exception($"Invalid SystemMessage: May only contain a single Text AIContent. Actual: {
+ String.Join(",", from contentObject in message.Contents select contentObject.GetType())
+ }");
+ }
+
+ systemMessagesSink.Add(SystemMessage.CreateSystemMessage(message.Text));
+ }
+
+ List contents = new(from rawContent in message.Contents select (DTO.ContentBase)rawContent);
+ return new DTO.ChatMessage(message.Role.ToString().ToLowerInvariant(), contents);
+ }
+
+ private ChatCompletionRequest CreateRequest(IList chatMessages, ChatOptions? options, bool requestStream)
+ {
+ ToolChoice? toolChoice = null;
+ ChatToolMode? mode = options?.ToolMode;
+
+ if (mode is AutoChatToolMode)
+ {
+ toolChoice = ToolChoice.Auto;
+ }
+ else if (mode is RequiredChatToolMode requiredToolMode)
+ {
+ if (requiredToolMode.RequiredFunctionName == null)
+ {
+ toolChoice = ToolChoice.Any;
+ }
+ else
+ {
+ toolChoice = ToolChoice.ToolUse(requiredToolMode.RequiredFunctionName!);
+ }
+ }
+
+ List systemMessages = new List();
+ List translatedMessages = new();
+
+ foreach (MEAI.ChatMessage message in chatMessages)
+ {
+ if (message.Role == ChatRole.System)
+ {
+ Translate(message, systemMessages);
+
+ // TODO: Should the system messages be included in the translatedMessages list?
+ }
+ else
+ {
+ translatedMessages.Add(Translate(message));
+ }
+ }
+
+ return new ChatCompletionRequest
+ {
+ Model = _modelId,
+
+ // TODO: We should consider coming up with a reasonable default for MaxTokens, since the MAAi APIs do not require
+ // it, while our wrapper for the Anthropic API does.
+ MaxTokens = options?.MaxOutputTokens ?? throw new ArgumentException("Must specify number of tokens in request for Anthropic", nameof(options)),
+ StopSequences = options?.StopSequences?.ToArray(),
+ Stream = requestStream,
+ Temperature = (decimal?)options?.Temperature, // TODO: why `decimal`?!
+ ToolChoice = toolChoice,
+ Tools = (from abstractTool in options?.Tools
+ where abstractTool is AIFunction
+ select (Tool)(AIFunction)abstractTool).ToList(),
+ TopK = options?.TopK,
+ TopP = (decimal?)options?.TopP,
+ SystemMessage = systemMessages.ToArray(),
+ Messages = translatedMessages,
+
+ // TODO: put these somewhere? .Metadata?
+ //ModelId = _modelId,
+ //Options = options
+ };
+ }
+
+ private sealed class ChatCompletionAccumulator
+ {
+ public string? CompletionId { get; set; }
+ public string? ModelId { get; set; }
+ public MEAI.ChatRole? StreamingRole { get; set; }
+ public MEAI.ChatFinishReason? FinishReason { get; set; }
+ // public DateTimeOffset CreatedAt { get; set; }
+
+ public ChatCompletionAccumulator() { }
+
+ public List? AccumulateAndExtractContent(ChatCompletionResponse response)
+ {
+ this.CompletionId ??= response.Id;
+ this.ModelId ??= response.Model;
+
+ this.FinishReason ??= response.StopReason switch
+ {
+ "end_turn" => MEAI.ChatFinishReason.Stop,
+ "stop_sequence" => MEAI.ChatFinishReason.Stop,
+ "tool_use" => MEAI.ChatFinishReason.ToolCalls,
+ "max_tokens" => MEAI.ChatFinishReason.Length,
+ _ => null
+ };
+
+ this.StreamingRole ??= response.Role switch
+ {
+ "assistant" => MEAI.ChatRole.Assistant,
+ //"user" => MEAI.ChatRole.User,
+ //null => null,
+ _ => throw new InvalidOperationException("Anthropic API is defined to only reply with 'assistant'.")
+ };
+
+ if (response.Content == null)
+ {
+ return null;
+ }
+
+ return new(from rawContent in response.Content select (MEAI.AIContent)rawContent);
+ }
+
+ }
+
+ private MEAI.ChatCompletion TranslateCompletion(ChatCompletionResponse response)
+ {
+ ChatCompletionAccumulator accumulator = new ChatCompletionAccumulator();
+ List? messageContents = accumulator.AccumulateAndExtractContent(response);
+
+ // According to the Anthropic API docs, the response will contain a single "option" in the MEAI
+ // parlance, but may contain multiple message? (I suspect for the purposes of tool use)
+ if (messageContents == null)
+ {
+ throw new ArgumentNullException(nameof(response.Content));
+ }
+ else if (messageContents.Count == 0)
+ {
+ throw new ArgumentException("Response did not contain any content", nameof(response));
+ }
+
+ MEAI.ChatMessage message = new(ChatRole.Assistant, messageContents);
+
+ return new MEAI.ChatCompletion(message)
+ {
+ CompletionId = accumulator.CompletionId,
+ ModelId = accumulator.ModelId,
+ //CreatedAt = TODO:
+ FinishReason = accumulator.FinishReason,
+ // Usage = TODO: extract this from the DTO
+ RawRepresentation = response
+ // WIP
+ };
+ }
+
+ private MEAI.StreamingChatCompletionUpdate TranslateStreamingUpdate(ChatCompletionAccumulator accumulator, ChatCompletionResponse response)
+ {
+ List? messageContents = accumulator.AccumulateAndExtractContent(response);
+
+ // messageContents will be non-null only on the final "tool_call" stop message update, which will contain
+ // all of the accumulated ToolUseContent objects.
+ if (messageContents == null && response.Delta != null && response.Delta.Type == "text_delta")
+ {
+ messageContents = new List { new MEAI.TextContent(response.Delta.Text) };
+ }
+
+ return new MEAI.StreamingChatCompletionUpdate
+ {
+ Role = accumulator.StreamingRole,
+ CompletionId = accumulator.CompletionId,
+ ModelId = accumulator.ModelId,
+ //CreatedAt = TODO:
+ FinishReason = accumulator.FinishReason,
+ //ChoiceIndex = 0,
+ Contents = messageContents,
+ RawRepresentation = response
+ };
+ }
+
+ public async Task CompleteAsync(IList chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ ChatCompletionRequest request = CreateRequest(chatMessages, options, requestStream: false);
+ ChatCompletionResponse response = await this.EnsureClient().CreateChatCompletionsAsync(request, cancellationToken);
+
+ return TranslateCompletion(response);
+ }
+
+ private AnthropicClient EnsureClient()
+ {
+ return this._anthropicClient ?? throw new ObjectDisposedException(nameof(AnthropicChatCompletionClient));
+ }
+
+ public async IAsyncEnumerable CompleteStreamingAsync(IList chatMessages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ ChatCompletionRequest request = CreateRequest(chatMessages, options, requestStream: true);
+ IAsyncEnumerable responseStream = this.EnsureClient().StreamingChatCompletionsAsync(request, cancellationToken);
+
+ // TODO: There is likely a better way to do this
+ ChatCompletionAccumulator accumulator = new();
+ await foreach (ChatCompletionResponse update in responseStream)
+ {
+ yield return TranslateStreamingUpdate(accumulator, update);
+ }
+ }
+
+ public void Dispose()
+ {
+ Interlocked.Exchange(ref this._anthropicClient, null)?.Dispose();
+ }
+
+ public TService? GetService(object? key = null) where TService : class
+ {
+ // Implement this based on the example in the M.E.AI.OpenAI implementation
+ // see: https://github.com/dotnet/extensions/blob/main/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs#L95-L105
+
+ if (key != null)
+ {
+ return null;
+ }
+
+ if (this is TService result)
+ {
+ return result;
+ }
+
+ if (typeof(TService).IsAssignableFrom(typeof(AnthropicClient)))
+ {
+ return (TService)(object)this._anthropicClient!;
+ }
+
+ return null;
+ }
+}
diff --git a/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicClient.cs
similarity index 90%
rename from dotnet/src/AutoGen.Anthropic/AnthropicClient.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicClient.cs
index f940864cec1c..b5ad91f10d4b 100644
--- a/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/AnthropicClient.cs
@@ -1,16 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// AnthropicClient.cs
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net.Http;
+using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
+
using AutoGen.Anthropic.Converters;
using AutoGen.Anthropic.DTO;
@@ -42,6 +38,8 @@ public AnthropicClient(HttpClient httpClient, string baseUrl, string apiKey)
_httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
}
+ internal string BaseUrl => _baseUrl;
+
public async Task CreateChatCompletionsAsync(ChatCompletionRequest chatCompletionRequest,
CancellationToken cancellationToken)
{
@@ -80,13 +78,18 @@ public async IAsyncEnumerable StreamingChatCompletionsAs
}
else // an empty line indicates the end of an event
{
+ Delta? initialText = null;
if (currentEvent.EventType == "content_block_start" && !string.IsNullOrEmpty(currentEvent.Data))
{
var dataBlock = JsonSerializer.Deserialize(currentEvent.Data!);
if (dataBlock != null && dataBlock.ContentBlock?.Type == "tool_use")
- {
+ { // TODO: verify we never get a non-empty text start content block
currentEvent.ContentBlock = dataBlock.ContentBlock;
}
+ else if (dataBlock != null && dataBlock.ContentBlock?.Type == "text")
+ {
+ initialText = new Delta { Type = "text_delta", Text = dataBlock.ContentBlock?.Text };
+ }
}
if (currentEvent.EventType is "message_start" or "content_block_delta" or "message_delta" && currentEvent.Data != null)
@@ -94,6 +97,12 @@ public async IAsyncEnumerable StreamingChatCompletionsAs
var res = await JsonSerializer.DeserializeAsync(
new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data)),
cancellationToken: cancellationToken) ?? throw new Exception("Failed to deserialize response");
+ if (initialText != null)
+ {
+ Debug.Assert(res.Delta == null, "content_block_start events should not also contain deltas");
+ res.Delta = initialText;
+ }
+
if (res.Delta?.Type == "input_json_delta" && !string.IsNullOrEmpty(res.Delta.PartialJson) &&
currentEvent.ContentBlock != null)
{
@@ -182,6 +191,9 @@ private sealed class ContentBlock
[JsonPropertyName("parameters")]
public string? Parameters { get; set; }
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
public void AppendDeltaParameters(string deltaParams)
{
StringBuilder sb = new StringBuilder(Parameters);
diff --git a/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/ContentBaseConverter.cs
similarity index 99%
rename from dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/ContentBaseConverter.cs
index 76c3200c1165..9062435243d3 100644
--- a/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/ContentBaseConverter.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ContentBaseConverter.cs
-using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using AutoGen.Anthropic.DTO;
+
namespace AutoGen.Anthropic.Converters;
public sealed class ContentBaseConverter : JsonConverter
diff --git a/dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/JsonPropertyNameEnumCoverter.cs
similarity index 98%
rename from dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/JsonPropertyNameEnumCoverter.cs
index 44ceb0718f3a..fc79f7d123f5 100644
--- a/dotnet/src/AutoGen.Anthropic/Converters/JsonPropertyNameEnumCoverter.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/JsonPropertyNameEnumCoverter.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// JsonPropertyNameEnumCoverter.cs
-using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
diff --git a/dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/SystemMessageConverter.cs
similarity index 98%
rename from dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/SystemMessageConverter.cs
index 0af3fa1a9059..97001a43f96d 100644
--- a/dotnet/src/AutoGen.Anthropic/Converters/SystemMessageConverter.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Converters/SystemMessageConverter.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SystemMessageConverter.cs
-using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using AutoGen.Anthropic.DTO;
diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionRequest.cs
similarity index 98%
rename from dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionRequest.cs
index c3f378dffe3a..3fef90a5b2c4 100644
--- a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionRequest.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ChatCompletionRequest.cs
-using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AutoGen.Anthropic.DTO;
diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionResponse.cs
similarity index 98%
rename from dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs
rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionResponse.cs
index 3b0135d38eb1..7b7b2cca417e 100644
--- a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ChatCompletionResponse.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// ChatCompletionResponse.cs
-using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace AutoGen.Anthropic.DTO;
+
public class ChatCompletionResponse
{
[JsonPropertyName("content")]
diff --git a/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Content.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Content.cs
new file mode 100644
index 000000000000..8f8bd53e84aa
--- /dev/null
+++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Content.cs
@@ -0,0 +1,280 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Content.cs
+
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using AutoGen.Anthropic.Converters;
+
+namespace AutoGen.Anthropic.DTO;
+
+public static class AIContentExtensions
+{
+ public static string? ToBase64String(this ReadOnlyMemory? data)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ // TODO: reduce the numbers of copies here ( .ToArray() + .ToBase64String() )
+ return Convert.ToBase64String(data.Value.ToArray());
+ }
+
+ public static byte[]? FromBase64String(this string? data)
+ {
+ if (data == null)
+ {
+ return null;
+ }
+
+ return Convert.FromBase64String(data);
+ }
+}
+
+public abstract class ContentBase
+{
+ [JsonPropertyName("type")]
+ public abstract string Type { get; }
+
+ [JsonPropertyName("cache_control")]
+ public CacheControl? CacheControl { get; set; }
+
+ public static implicit operator ContentBase(Microsoft.Extensions.AI.AIContent content)
+ {
+ return content switch
+ {
+ Microsoft.Extensions.AI.TextContent textContent => (TextContent)textContent,
+ Microsoft.Extensions.AI.ImageContent imageContent => (ImageContent)imageContent,
+ Microsoft.Extensions.AI.FunctionCallContent functionCallContent => (ToolUseContent)functionCallContent,
+ Microsoft.Extensions.AI.FunctionResultContent functionResultContent => (ToolResultContent)functionResultContent,
+ _ => throw new NotSupportedException($"Unsupported content type: {content.GetType()}")
+ };
+ }
+
+ public static implicit operator Microsoft.Extensions.AI.AIContent(ContentBase content)
+ {
+ return content switch
+ {
+ TextContent textContent => (Microsoft.Extensions.AI.TextContent)textContent,
+ ImageContent imageContent => (Microsoft.Extensions.AI.ImageContent)imageContent,
+ ToolUseContent toolUseContent => (Microsoft.Extensions.AI.FunctionCallContent)toolUseContent,
+ ToolResultContent toolResultContent => (Microsoft.Extensions.AI.FunctionResultContent)toolResultContent,
+ _ => throw new NotSupportedException($"Unsupported content type: {content.GetType()}")
+ };
+ }
+}
+
+public class TextContent : ContentBase
+{
+ [JsonPropertyName("type")]
+ public override string Type => "text";
+
+ [JsonPropertyName("text")]
+ public string? Text { get; set; }
+
+ public static TextContent CreateTextWithCacheControl(string text) => new()
+ {
+ Text = text,
+ CacheControl = new CacheControl { Type = CacheControlType.Ephemeral }
+ };
+
+ public static implicit operator TextContent(Microsoft.Extensions.AI.TextContent textContent)
+ {
+ return new TextContent { Text = textContent.Text };
+ }
+
+ public static implicit operator Microsoft.Extensions.AI.TextContent(TextContent textContent)
+ {
+ return new Microsoft.Extensions.AI.TextContent(textContent.Text)
+ {
+ RawRepresentation = textContent
+ };
+ }
+}
+
+public class ImageContent : ContentBase
+{
+ [JsonPropertyName("type")]
+ public override string Type => "image";
+
+ [JsonPropertyName("source")]
+ public ImageSource? Source { get; set; }
+
+ public static implicit operator ImageContent(Microsoft.Extensions.AI.ImageContent imageContent)
+ {
+ ImageSource source = new ImageSource
+ {
+ MediaType = imageContent.MediaType,
+ };
+
+ if (imageContent.ContainsData)
+ {
+ source.Data = imageContent.Data.ToBase64String();
+ }
+
+ return new ImageContent
+ {
+ Source = source,
+ };
+ }
+
+ public static implicit operator Microsoft.Extensions.AI.ImageContent(ImageContent imageContent)
+ {
+ ReadOnlyMemory imageData = imageContent.Source?.Data.FromBase64String() ?? [];
+
+ return new Microsoft.Extensions.AI.ImageContent(imageData, mediaType: imageContent.Source?.MediaType)
+ {
+ RawRepresentation = imageContent
+ };
+ }
+}
+
+public class ImageSource
+{
+ [JsonPropertyName("type")]
+ public string Type => "base64";
+
+ [JsonPropertyName("media_type")]
+ public string? MediaType { get; set; }
+
+ [JsonPropertyName("data")]
+ public string? Data { get; set; }
+}
+
+public class ToolUseContent : ContentBase
+{
+ [JsonPropertyName("type")]
+ public override string Type => "tool_use";
+
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("input")]
+ public JsonNode? Input { get; set; }
+
+ public static implicit operator ToolUseContent(Microsoft.Extensions.AI.FunctionCallContent functionCallContent)
+ {
+ JsonNode? input = functionCallContent.Arguments != null ? JsonSerializer.SerializeToNode(functionCallContent.Arguments) : null;
+
+ return new ToolUseContent
+ {
+ Id = functionCallContent.CallId,
+ Name = functionCallContent.Name,
+ Input = input
+ };
+ }
+
+ public static implicit operator Microsoft.Extensions.AI.FunctionCallContent(ToolUseContent toolUseContent)
+ {
+ // These are an unfortunate incompatibilty between the two libraries (for now); later we can work to
+ // parse the JSON directly into the M.E.AI types
+ if (toolUseContent.Id == null)
+ {
+ throw new ArgumentNullException(nameof(toolUseContent.Id));
+ }
+
+ if (toolUseContent.Name == null)
+ {
+ throw new ArgumentNullException(nameof(toolUseContent.Name));
+ }
+
+ IDictionary? arguments = null;
+ if (toolUseContent.Input != null)
+ {
+ arguments = JsonSerializer.Deserialize>(toolUseContent.Input);
+ }
+
+ return new Microsoft.Extensions.AI.FunctionCallContent(toolUseContent.Id, toolUseContent.Name, arguments)
+ {
+ RawRepresentation = toolUseContent
+ };
+ }
+}
+
+public class ToolResultContent : ContentBase
+{
+ [JsonPropertyName("type")]
+ public override string Type => "tool_result";
+
+ [JsonPropertyName("tool_use_id")]
+ public string? Id { get; set; }
+
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ [JsonPropertyName("is_error")]
+ public bool IsError { get; set; }
+
+ public static implicit operator ToolResultContent(Microsoft.Extensions.AI.FunctionResultContent functionResultContent)
+ {
+ // If the result is successful, convert the return object (if any) to the content string
+ // Otherwise, convert the error message to the content string
+ string? content = null;
+ if (functionResultContent.Exception != null)
+ {
+ // TODO: Technically, .Result should also contain the error message?
+ content = functionResultContent.Exception.Message;
+ }
+ else if (functionResultContent.Result != null)
+ {
+ // If the result is a string, it should just be a passthrough (with enquotation)
+ content = JsonSerializer.Serialize(functionResultContent.Result);
+ }
+
+ return new ToolResultContent
+ {
+ Id = functionResultContent.CallId,
+ Content = content,
+ IsError = functionResultContent.Exception != null
+ };
+ }
+
+ public static implicit operator Microsoft.Extensions.AI.FunctionResultContent(ToolResultContent toolResultContent)
+ {
+ if (toolResultContent.Id == null)
+ {
+ throw new ArgumentNullException(nameof(toolResultContent.Id));
+ }
+
+ // If the content is a string, it should be deserialized as a string
+ object? result = null;
+ if (toolResultContent.Content != null)
+ {
+ // TODO: Unfortunately, there is no way to get back to the exception from the content,
+ // since ToolCallResult does not encode a way to determine success from failure
+ result = JsonSerializer.Deserialize