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(toolResultContent.Content); + } + + Exception? error = null; + if (toolResultContent.IsError) + { + error = new Exception(toolResultContent.Content); + } + + // TODO: Should we model the name on this object? + return new Microsoft.Extensions.AI.FunctionResultContent(toolResultContent.Id, "", result) + { + Exception = error, + RawRepresentation = toolResultContent + }; + } +} + +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/ErrorResponse.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ErrorResponse.cs similarity index 100% rename from dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ErrorResponse.cs diff --git a/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Tool.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Tool.cs new file mode 100644 index 000000000000..f47a10f0fa77 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/Tool.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Tool.cs + +using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; + +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; } + + // Implicit conversion operator from M.E.AI.AITool to Tool + public static implicit operator Tool(Microsoft.Extensions.AI.AIFunction tool) + { + return new Tool + { + Name = tool.Metadata.Name, + Description = tool.Metadata.Description, + InputSchema = InputSchema.ExtractSchema(tool), + //CacheControl = null + }; + } +} + +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 static InputSchema ExtractSchema(AIFunction tool) => ExtractSchema(tool.Metadata.Parameters); + + private static InputSchema ExtractSchema(IReadOnlyList parameterMetadata) + { + List required = new List(); + Dictionary properties = new Dictionary(); + + foreach (AIFunctionParameterMetadata parameter in parameterMetadata) + { + properties.Add(parameter.Name, new SchemaProperty { Type = parameter.ParameterType?.Name, Description = parameter.Description }); + if (parameter.IsRequired) + { + required.Add(parameter.Name); + } + } + + return new InputSchema { Type = "object", Properties = properties, Required = required }; + } +} + +public class SchemaProperty +{ + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ToolChoice.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ToolChoice.cs similarity index 100% rename from dotnet/src/AutoGen.Anthropic/DTO/ToolChoice.cs rename to dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/DTO/ToolChoice.cs diff --git a/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Microsoft.AutoGen.Extensions.Anthropic.csproj b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Microsoft.AutoGen.Extensions.Anthropic.csproj new file mode 100644 index 000000000000..4674bb55ad29 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Microsoft.AutoGen.Extensions.Anthropic.csproj @@ -0,0 +1,31 @@ + + + $(PackageTargetFrameworks) + enable + enable + + + + + + + Microsoft.Autogen.Extensions.Anthropic + + Provide support for consuming Anthropic models in AutoGen + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Utils/AnthropicConstants.cs b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Utils/AnthropicConstants.cs new file mode 100644 index 000000000000..305d2bbe7d81 --- /dev/null +++ b/dotnet/src/Microsoft.AutoGen/Extensions/Anthropic/Utils/AnthropicConstants.cs @@ -0,0 +1,30 @@ +// 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"; + } +} + +namespace Microsoft.AutoGen.Extensions.Anthropic.Utils +{ + public static class AnthropicConstants + { + public static string Endpoint = global::AutoGen.Anthropic.Utils.AnthropicConstants.Endpoint; + + // Models + public static string Claude3Opus = global::AutoGen.Anthropic.Utils.AnthropicConstants.Claude3Opus; + public static string Claude3Sonnet = global::AutoGen.Anthropic.Utils.AnthropicConstants.Claude3Sonnet; + public static string Claude3Haiku = global::AutoGen.Anthropic.Utils.AnthropicConstants.Claude3Haiku; + public static string Claude35Sonnet = global::AutoGen.Anthropic.Utils.AnthropicConstants.Claude35Sonnet; + } +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs index 91097d7485df..cc33f6e23b92 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs @@ -8,6 +8,8 @@ using AutoGen.Tests; using FluentAssertions; +using Microsoft.AutoGen.Extensions.Anthropic.Tests; + namespace AutoGen.Anthropic.Tests; public class AnthropicClientAgentTest diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs index c98a81079854..a1b41d7adc3c 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs @@ -5,13 +5,15 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; -using AutoGen.Anthropic.DTO; -using AutoGen.Anthropic.Utils; using AutoGen.Tests; using FluentAssertions; using Xunit; -namespace AutoGen.Anthropic.Tests; +using AutoGen.Anthropic; +using AutoGen.Anthropic.DTO; +using AutoGen.Anthropic.Utils; + +namespace Microsoft.AutoGen.Extensions.Anthropic.Tests; public class AnthropicClientTests { diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs index b753aa8ab0cf..dbe39dc43377 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestFunctionCalls.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; using AutoGen.Core; -namespace AutoGen.Anthropic.Tests; +namespace Microsoft.AutoGen.Extensions.Anthropic.Tests; public partial class AnthropicTestFunctionCalls { diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs index 6849733ff652..1c5dd6228a4d 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs @@ -3,7 +3,7 @@ using AutoGen.Anthropic.DTO; -namespace AutoGen.Anthropic.Tests; +namespace Microsoft.AutoGen.Extensions.Anthropic.Tests; public static class AnthropicTestUtils { diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj index ac9617c1a573..d1bdce04e326 100644 --- a/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj +++ b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj @@ -11,6 +11,7 @@ +