diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index be40e7b61b6..af136757410 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -38,6 +38,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Ollama", "src\AutoGen.Ollama\AutoGen.Ollama.csproj", "{9F9E6DED-3D92-4970-909A-70FC11F1A665}" EndProject @@ -46,6 +49,12 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Ollama.Sample", "sample\AutoGen.Ollama.Sample\AutoGen.Ollama.Sample.csproj", "{93AA4D0D-6EE4-44D5-AD77-7F73A3934544}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.SemanticKernel.Sample", "sample\AutoGen.SemanticKernel.Sample\AutoGen.SemanticKernel.Sample.csproj", "{52958A60-3FF7-4243-9058-34A6E4F55C31}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Anthropic", "src\AutoGen.Anthropic\AutoGen.Anthropic.csproj", "{6A95E113-B824-4524-8F13-CD0C3E1C8804}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Anthropic.Tests", "test\AutoGen.Anthropic.Tests\AutoGen.Anthropic.Tests.csproj", "{815E937E-86D6-4476-9EC6-B7FBCBBB5DB6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Anthropic.Samples", "sample\AutoGen.Anthropic.Samples\AutoGen.Anthropic.Samples.csproj", "{834B4E85-64E5-4382-8465-548F332E5298}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +137,18 @@ Global {52958A60-3FF7-4243-9058-34A6E4F55C31}.Debug|Any CPU.Build.0 = Debug|Any CPU {52958A60-3FF7-4243-9058-34A6E4F55C31}.Release|Any CPU.ActiveCfg = Release|Any CPU {52958A60-3FF7-4243-9058-34A6E4F55C31}.Release|Any CPU.Build.0 = Release|Any CPU + {6A95E113-B824-4524-8F13-CD0C3E1C8804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A95E113-B824-4524-8F13-CD0C3E1C8804}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A95E113-B824-4524-8F13-CD0C3E1C8804}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A95E113-B824-4524-8F13-CD0C3E1C8804}.Release|Any CPU.Build.0 = Release|Any CPU + {815E937E-86D6-4476-9EC6-B7FBCBBB5DB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {815E937E-86D6-4476-9EC6-B7FBCBBB5DB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {815E937E-86D6-4476-9EC6-B7FBCBBB5DB6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {815E937E-86D6-4476-9EC6-B7FBCBBB5DB6}.Release|Any CPU.Build.0 = Release|Any CPU + {834B4E85-64E5-4382-8465-548F332E5298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {834B4E85-64E5-4382-8465-548F332E5298}.Debug|Any CPU.Build.0 = Debug|Any CPU + {834B4E85-64E5-4382-8465-548F332E5298}.Release|Any CPU.ActiveCfg = Release|Any CPU + {834B4E85-64E5-4382-8465-548F332E5298}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -148,6 +169,9 @@ Global {1DFABC4A-8458-4875-8DCB-59F3802DAC65} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {D36A85F9-C172-487D-8192-6BFE5D05B4A7} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {6A95E113-B824-4524-8F13-CD0C3E1C8804} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {815E937E-86D6-4476-9EC6-B7FBCBBB5DB6} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {834B4E85-64E5-4382-8465-548F332E5298} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} {9F9E6DED-3D92-4970-909A-70FC11F1A665} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {03E31CAA-3728-48D3-B936-9F11CF6C18FE} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {93AA4D0D-6EE4-44D5-AD77-7F73A3934544} = {FBFEAD1F-29EB-4D99-A672-0CD8473E10B9} diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs b/dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs new file mode 100644 index 00000000000..94b5f37511e --- /dev/null +++ b/dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicSamples.cs + +using AutoGen.Anthropic.Extensions; +using AutoGen.Anthropic.Utils; +using AutoGen.Core; + +namespace AutoGen.Anthropic.Samples; + +public static class AnthropicSamples +{ + public static async Task RunAsync() + { + #region create_anthropic_agent + var apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? throw new Exception("Missing ANTHROPIC_API_KEY environment variable."); + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, apiKey); + var agent = new AnthropicClientAgent(anthropicClient, "assistant", AnthropicConstants.Claude3Haiku); + #endregion + + #region register_middleware + var agentWithConnector = agent + .RegisterMessageConnector() + .RegisterPrintMessage(); + #endregion register_middleware + + await agentWithConnector.SendAsync(new TextMessage(Role.Assistant, "Hello", from: "user")); + } +} diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj b/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj new file mode 100644 index 00000000000..33a5aa7f16b --- /dev/null +++ b/dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj @@ -0,0 +1,18 @@ + + + + Exe + $(TestTargetFramework) + enable + enable + True + + + + + + + + + + diff --git a/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs b/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs new file mode 100644 index 00000000000..f3c61508861 --- /dev/null +++ b/dotnet/sample/AutoGen.Anthropic.Samples/Program.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Program.cs + +namespace AutoGen.Anthropic.Samples; + +internal static class Program +{ + public static async Task Main(string[] args) + { + await AnthropicSamples.RunAsync(); + } +} diff --git a/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs new file mode 100644 index 00000000000..e395bb4a225 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Anthropic.DTO; +using AutoGen.Core; + +namespace AutoGen.Anthropic; + +public class AnthropicClientAgent : IStreamingAgent +{ + private readonly AnthropicClient _anthropicClient; + public string Name { get; } + private readonly string _modelName; + private readonly string _systemMessage; + private readonly decimal _temperature; + private readonly int _maxTokens; + + public AnthropicClientAgent( + AnthropicClient anthropicClient, + string name, + string modelName, + string systemMessage = "You are a helpful AI assistant", + decimal temperature = 0.7m, + int maxTokens = 1024) + { + Name = name; + _anthropicClient = anthropicClient; + _modelName = modelName; + _systemMessage = systemMessage; + _temperature = temperature; + _maxTokens = maxTokens; + } + + public async Task GenerateReplyAsync(IEnumerable messages, GenerateReplyOptions? options = null, + CancellationToken cancellationToken = default) + { + var response = await _anthropicClient.CreateChatCompletionsAsync(CreateParameters(messages, options, false), cancellationToken); + return new MessageEnvelope(response, from: this.Name); + } + + public async IAsyncEnumerable GenerateStreamingReplyAsync(IEnumerable messages, + GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var message in _anthropicClient.StreamingChatCompletionsAsync( + CreateParameters(messages, options, true), cancellationToken)) + { + yield return new MessageEnvelope(message, from: this.Name); + } + } + + private ChatCompletionRequest CreateParameters(IEnumerable messages, GenerateReplyOptions? options, bool shouldStream) + { + var chatCompletionRequest = new ChatCompletionRequest() + { + SystemMessage = _systemMessage, + MaxTokens = options?.MaxToken ?? _maxTokens, + Model = _modelName, + Stream = shouldStream, + Temperature = (decimal?)options?.Temperature ?? _temperature, + }; + + chatCompletionRequest.Messages = BuildMessages(messages); + + return chatCompletionRequest; + } + + private List BuildMessages(IEnumerable messages) + { + List chatMessages = new(); + foreach (IMessage? message in messages) + { + switch (message) + { + case IMessage chatMessage when chatMessage.Content.Role == "system": + throw new InvalidOperationException( + "system message has already been set and only one system message is supported. \"system\" role for input messages in the Message"); + + case IMessage chatMessage: + chatMessages.Add(chatMessage.Content); + break; + + default: + throw new ArgumentException($"Unexpected message type: {message?.GetType()}"); + } + } + + return chatMessages; + } +} diff --git a/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs b/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs new file mode 100644 index 00000000000..8ea0bef86e2 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/AnthropicClient.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicClient.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +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; + +namespace AutoGen.Anthropic; + +public sealed class AnthropicClient : IDisposable +{ + private readonly HttpClient _httpClient; + private readonly string _baseUrl; + + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + private static readonly JsonSerializerOptions JsonDeserializerOptions = new() + { + Converters = { new ContentBaseConverter() } + }; + + public AnthropicClient(HttpClient httpClient, string baseUrl, string apiKey) + { + _httpClient = httpClient; + _baseUrl = baseUrl; + + _httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey); + _httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); + } + + public async Task CreateChatCompletionsAsync(ChatCompletionRequest chatCompletionRequest, + CancellationToken cancellationToken) + { + var httpResponseMessage = await SendRequestAsync(chatCompletionRequest, cancellationToken); + var responseStream = await httpResponseMessage.Content.ReadAsStreamAsync(); + + if (httpResponseMessage.IsSuccessStatusCode) + return await DeserializeResponseAsync(responseStream, cancellationToken); + + ErrorResponse res = await DeserializeResponseAsync(responseStream, cancellationToken); + throw new Exception(res.Error?.Message); + } + + public async IAsyncEnumerable StreamingChatCompletionsAsync( + ChatCompletionRequest chatCompletionRequest, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var httpResponseMessage = await SendRequestAsync(chatCompletionRequest, cancellationToken); + using var reader = new StreamReader(await httpResponseMessage.Content.ReadAsStreamAsync()); + + var currentEvent = new SseEvent(); + while (await reader.ReadLineAsync() is { } line) + { + if (!string.IsNullOrEmpty(line)) + { + currentEvent.Data = line.Substring("data:".Length).Trim(); + } + else + { + if (currentEvent.Data == "[DONE]") + continue; + + if (currentEvent.Data != null) + { + yield return await JsonSerializer.DeserializeAsync( + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data)), + cancellationToken: cancellationToken) ?? throw new Exception("Failed to deserialize response"); + } + else if (currentEvent.Data != null) + { + var res = await JsonSerializer.DeserializeAsync( + new MemoryStream(Encoding.UTF8.GetBytes(currentEvent.Data)), cancellationToken: cancellationToken); + + throw new Exception(res?.Error?.Message); + } + + // Reset the current event for the next one + currentEvent = new SseEvent(); + } + } + } + + private Task SendRequestAsync(T requestObject, CancellationToken cancellationToken) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, _baseUrl); + var jsonRequest = JsonSerializer.Serialize(requestObject, JsonSerializerOptions); + httpRequestMessage.Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); + return _httpClient.SendAsync(httpRequestMessage, cancellationToken); + } + + private async Task DeserializeResponseAsync(Stream responseStream, CancellationToken cancellationToken) + { + return await JsonSerializer.DeserializeAsync(responseStream, JsonDeserializerOptions, cancellationToken) + ?? throw new Exception("Failed to deserialize response"); + } + + public void Dispose() + { + _httpClient.Dispose(); + } + + private struct SseEvent + { + public string? Data { get; set; } + + public SseEvent(string? data = null) + { + Data = data; + } + } +} diff --git a/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj new file mode 100644 index 00000000000..fefc439e00b --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + AutoGen.Anthropic + + + + + + + AutoGen.Anthropic + + Provide support for consuming Anthropic models in AutoGen + + + + + + + + diff --git a/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs b/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs new file mode 100644 index 00000000000..281274048ed --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ContentConverter.cs + +using AutoGen.Anthropic.DTO; + +namespace AutoGen.Anthropic.Converters; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +public sealed class ContentBaseConverter : JsonConverter +{ + public override ContentBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + if (doc.RootElement.TryGetProperty("type", out JsonElement typeProperty) && !string.IsNullOrEmpty(typeProperty.GetString())) + { + string? type = typeProperty.GetString(); + var text = doc.RootElement.GetRawText(); + switch (type) + { + case "text": + return JsonSerializer.Deserialize(text, options) ?? throw new InvalidOperationException(); + case "image": + return JsonSerializer.Deserialize(text, options) ?? throw new InvalidOperationException(); + } + } + + throw new JsonException("Unknown content type"); + } + + public override void Write(Utf8JsonWriter writer, ContentBase value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs new file mode 100644 index 00000000000..fa1654bc11d --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionRequest.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +using System.Text.Json.Serialization; + +namespace AutoGen.Anthropic.DTO; + +using System.Collections.Generic; + +public class ChatCompletionRequest +{ + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("messages")] + public List Messages { get; set; } + + [JsonPropertyName("system")] + public string? SystemMessage { get; set; } + + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; set; } + + [JsonPropertyName("metadata")] + public object? Metadata { get; set; } + + [JsonPropertyName("stop_sequences")] + public string[]? StopSequences { get; set; } + + [JsonPropertyName("stream")] + public bool? Stream { get; set; } + + [JsonPropertyName("temperature")] + public decimal? Temperature { get; set; } + + [JsonPropertyName("top_k")] + public int? TopK { get; set; } + + [JsonPropertyName("top_p")] + public decimal? TopP { get; set; } + + public ChatCompletionRequest() + { + Messages = new List(); + } +} + +public class ChatMessage +{ + [JsonPropertyName("role")] + public string Role { get; set; } + + [JsonPropertyName("content")] + public string Content { get; set; } + + public ChatMessage(string role, string content) + { + Role = role; + Content = content; + } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs new file mode 100644 index 00000000000..c6861f9c315 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/ChatCompletionResponse.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +namespace AutoGen.Anthropic.DTO; + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +public class ChatCompletionResponse +{ + [JsonPropertyName("content")] + public List? Content { get; set; } + + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("role")] + public string? Role { get; set; } + + [JsonPropertyName("stop_reason")] + public string? StopReason { get; set; } + + [JsonPropertyName("stop_sequence")] + public object? StopSequence { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } + + [JsonPropertyName("delta")] + public Delta? Delta { get; set; } + + [JsonPropertyName("message")] + public StreamingMessage? streamingMessage { get; set; } +} + +public class StreamingMessage +{ + [JsonPropertyName("id")] + public string? Id { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("role")] + public string? Role { get; set; } + + [JsonPropertyName("content")] + public List? Content { get; set; } + + [JsonPropertyName("model")] + public string? Model { get; set; } + + [JsonPropertyName("stop_reason")] + public object? StopReason { get; set; } + + [JsonPropertyName("stop_sequence")] + public object? StopSequence { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } +} + +public class Usage +{ + [JsonPropertyName("input_tokens")] + public int InputTokens { get; set; } + + [JsonPropertyName("output_tokens")] + public int OutputTokens { get; set; } +} + +public class Delta +{ + [JsonPropertyName("stop_reason")] + public string? StopReason { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } + + [JsonPropertyName("text")] + public string? Text { get; set; } + + [JsonPropertyName("usage")] + public Usage? Usage { get; set; } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/Content.cs b/dotnet/src/AutoGen.Anthropic/DTO/Content.cs new file mode 100644 index 00000000000..dd2481bd58f --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/Content.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Content.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Anthropic.DTO; + +public abstract class ContentBase +{ + [JsonPropertyName("type")] + public abstract string Type { get; } +} + +public class TextContent : ContentBase +{ + [JsonPropertyName("type")] + public override string Type => "text"; + + [JsonPropertyName("text")] + public string? Text { get; set; } +} + +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; } +} diff --git a/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs b/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs new file mode 100644 index 00000000000..d02a8f6d1cf --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/DTO/ErrorResponse.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ErrorResponse.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Anthropic.DTO; + +public sealed class ErrorResponse +{ + [JsonPropertyName("error")] + public Error? Error { get; set; } +} + +public sealed class Error +{ + [JsonPropertyName("Type")] + public string? Type { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } +} diff --git a/dotnet/src/AutoGen.Anthropic/Extensions/AnthropicAgentExtension.cs b/dotnet/src/AutoGen.Anthropic/Extensions/AnthropicAgentExtension.cs new file mode 100644 index 00000000000..35ea8ed190a --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Extensions/AnthropicAgentExtension.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicAgentExtension.cs + +using AutoGen.Anthropic.Middleware; +using AutoGen.Core; + +namespace AutoGen.Anthropic.Extensions; + +public static class AnthropicAgentExtension +{ + /// + /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this AnthropicClientAgent agent, AnthropicMessageConnector? connector = null) + { + connector ??= new AnthropicMessageConnector(); + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, AnthropicMessageConnector? connector = null) + { + connector ??= new AnthropicMessageConnector(); + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs b/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs new file mode 100644 index 00000000000..bfe79190925 --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Middleware/AnthropicMessageConnector.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Anthropic.DTO; +using AutoGen.Core; + +namespace AutoGen.Anthropic.Middleware; + +public class AnthropicMessageConnector : IStreamingMiddleware +{ + public string? Name => nameof(AnthropicMessageConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var chatMessages = ProcessMessage(messages, agent); + var response = await agent.GenerateReplyAsync(chatMessages, context.Options, cancellationToken); + + return response is IMessage chatMessage + ? PostProcessMessage(chatMessage.Content, agent) + : response; + } + + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var messages = context.Messages; + var chatMessages = ProcessMessage(messages, agent); + + await foreach (var reply in agent.GenerateStreamingReplyAsync(chatMessages, context.Options, cancellationToken)) + { + if (reply is IStreamingMessage chatMessage) + { + var response = ProcessChatCompletionResponse(chatMessage, agent); + if (response is not null) + { + yield return response; + } + } + else + { + yield return reply; + } + } + } + + private IStreamingMessage? ProcessChatCompletionResponse(IStreamingMessage chatMessage, + IStreamingAgent agent) + { + Delta? delta = chatMessage.Content.Delta; + return delta != null && !string.IsNullOrEmpty(delta.Text) + ? new TextMessageUpdate(role: Role.Assistant, delta.Text, from: agent.Name) + : null; + } + + private IEnumerable ProcessMessage(IEnumerable messages, IAgent agent) + { + return messages.SelectMany(m => + { + return m switch + { + TextMessage textMessage => ProcessTextMessage(textMessage, agent), + _ => [m], + }; + }); + } + + private IMessage PostProcessMessage(ChatCompletionResponse response, IAgent from) + { + if (response.Content is null) + throw new ArgumentNullException(nameof(response.Content)); + + if (response.Content.Count != 1) + throw new NotSupportedException($"{nameof(response.Content)} != 1"); + + return new TextMessage(Role.Assistant, ((TextContent)response.Content[0]).Text ?? string.Empty, from: from.Name); + } + + private IEnumerable> ProcessTextMessage(TextMessage textMessage, IAgent agent) + { + IEnumerable messages; + + if (textMessage.From == agent.Name) + { + messages = [new ChatMessage( + "assistant", textMessage.Content)]; + } + else if (textMessage.From is null) + { + if (textMessage.Role == Role.User) + { + messages = [new ChatMessage( + "user", textMessage.Content)]; + } + else if (textMessage.Role == Role.Assistant) + { + messages = [new ChatMessage( + "assistant", textMessage.Content)]; + } + else if (textMessage.Role == Role.System) + { + messages = [new ChatMessage( + "system", textMessage.Content)]; + } + else + { + throw new NotSupportedException($"Role {textMessage.Role} is not supported"); + } + } + else + { + // if from is not null, then the message is from user + messages = [new ChatMessage( + "user", textMessage.Content)]; + } + + return messages.Select(m => new MessageEnvelope(m, from: textMessage.From)); + } +} diff --git a/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs b/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs new file mode 100644 index 00000000000..e70572cbddf --- /dev/null +++ b/dotnet/src/AutoGen.Anthropic/Utils/AnthropicConstants.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Constants.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"; +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs new file mode 100644 index 00000000000..ba31f2297ba --- /dev/null +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientAgentTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicClientAgentTest.cs + +using AutoGen.Anthropic.Extensions; +using AutoGen.Anthropic.Utils; +using AutoGen.Tests; +using Xunit.Abstractions; + +namespace AutoGen.Anthropic; + +public class AnthropicClientAgentTest +{ + private readonly ITestOutputHelper _output; + + public AnthropicClientAgentTest(ITestOutputHelper output) => _output = output; + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicAgentChatCompletionTestAsync() + { + var client = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var agent = new AnthropicClientAgent( + client, + name: "AnthropicAgent", + AnthropicConstants.Claude3Haiku).RegisterMessageConnector(); + + var singleAgentTest = new SingleAgentTest(_output); + await singleAgentTest.UpperCaseTestAsync(agent); + await singleAgentTest.UpperCaseStreamingTestAsync(agent); + } +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs new file mode 100644 index 00000000000..0b64c9e4e3c --- /dev/null +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicClientTest.cs @@ -0,0 +1,87 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using AutoGen.Anthropic.DTO; +using AutoGen.Anthropic.Utils; +using AutoGen.Tests; +using FluentAssertions; +using Xunit; + +namespace AutoGen.Anthropic; + +public class AnthropicClientTests +{ + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicClientChatCompletionTestAsync() + { + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var request = new ChatCompletionRequest(); + request.Model = AnthropicConstants.Claude3Haiku; + request.Stream = false; + request.MaxTokens = 100; + request.Messages = new List() { new ChatMessage("user", "Hello world") }; + ChatCompletionResponse response = await anthropicClient.CreateChatCompletionsAsync(request, CancellationToken.None); + + Assert.NotNull(response); + Assert.NotNull(response.Content); + Assert.NotEmpty(response.Content); + response.Content.Count.Should().Be(1); + response.Content.First().Should().BeOfType(); + var textContent = (TextContent)response.Content.First(); + Assert.Equal("text", textContent.Type); + Assert.NotNull(response.Usage); + response.Usage.OutputTokens.Should().BeGreaterThan(0); + } + + [ApiKeyFact("ANTHROPIC_API_KEY")] + public async Task AnthropicClientStreamingChatCompletionTestAsync() + { + var anthropicClient = new AnthropicClient(new HttpClient(), AnthropicConstants.Endpoint, AnthropicTestUtils.ApiKey); + + var request = new ChatCompletionRequest(); + request.Model = AnthropicConstants.Claude3Haiku; + request.Stream = true; + request.MaxTokens = 500; + request.SystemMessage = "You are a helpful assistant that convert input to json object"; + request.Messages = new List() + { + new("user", "name: John, age: 41, email: g123456@gmail.com") + }; + + var response = anthropicClient.StreamingChatCompletionsAsync(request, CancellationToken.None); + var results = await response.ToListAsync(); + results.Count.Should().BeGreaterThan(0); + + // Merge the chunks. + StringBuilder sb = new(); + foreach (ChatCompletionResponse result in results) + { + if (result.Delta is not null && !string.IsNullOrEmpty(result.Delta.Text)) + sb.Append(result.Delta.Text); + } + + string resultContent = sb.ToString(); + Assert.NotNull(resultContent); + + var person = JsonSerializer.Deserialize(resultContent); + Assert.NotNull(person); + person.Name.Should().Be("John"); + person.Age.Should().Be(41); + person.Email.Should().Be("g123456@gmail.com"); + Assert.NotNull(results.First().streamingMessage); + results.First().streamingMessage!.Role.Should().Be("assistant"); + } + + private sealed class Person + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("age")] + public int Age { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } = string.Empty; + } +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs new file mode 100644 index 00000000000..a5b80eee3bd --- /dev/null +++ b/dotnet/test/AutoGen.Anthropic.Tests/AnthropicTestUtils.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// AnthropicTestUtils.cs + +namespace AutoGen.Anthropic; + +public static class AnthropicTestUtils +{ + public static string ApiKey => Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") ?? + throw new Exception("Please set ANTHROPIC_API_KEY environment variable."); +} diff --git a/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj new file mode 100644 index 00000000000..8cd1e3003b0 --- /dev/null +++ b/dotnet/test/AutoGen.Anthropic.Tests/AutoGen.Anthropic.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(TestTargetFramework) + enable + false + True + AutoGen.Anthropic.Tests + + + + + + + + + + + + + + +