Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce AnthropicClient and AnthropicClientAgent #2769

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions dotnet/AutoGen.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
28 changes: 28 additions & 0 deletions dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TestTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AutoGen.Anthropic\AutoGen.Anthropic.csproj" />
<ProjectReference Include="..\..\src\AutoGen.DotnetInteractive\AutoGen.DotnetInteractive.csproj" />
<ProjectReference Include="..\..\src\AutoGen.SourceGenerator\AutoGen.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\src\AutoGen\AutoGen.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions dotnet/sample/AutoGen.Anthropic.Samples/Program.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
91 changes: 91 additions & 0 deletions dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs
Original file line number Diff line number Diff line change
@@ -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<IMessage> GenerateReplyAsync(IEnumerable<IMessage> messages, GenerateReplyOptions? options = null,
CancellationToken cancellationToken = default)
{
var response = await _anthropicClient.CreateChatCompletionsAsync(CreateParameters(messages, options, false), cancellationToken);
return new MessageEnvelope<ChatCompletionResponse>(response, from: this.Name);
}

public async IAsyncEnumerable<IStreamingMessage> GenerateStreamingReplyAsync(IEnumerable<IMessage> messages,
GenerateReplyOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var message in _anthropicClient.StreamingChatCompletionsAsync(
CreateParameters(messages, options, true), cancellationToken))
{
yield return new MessageEnvelope<ChatCompletionResponse>(message, from: this.Name);
}
}

private ChatCompletionRequest CreateParameters(IEnumerable<IMessage> 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<ChatMessage> BuildMessages(IEnumerable<IMessage> messages)
{
List<ChatMessage> chatMessages = new();
foreach (IMessage? message in messages)
{
switch (message)
{
case IMessage<ChatMessage> chatMessage when chatMessage.Content.Role == "system":
DavidLuong98 marked this conversation as resolved.
Show resolved Hide resolved
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> chatMessage:
chatMessages.Add(chatMessage.Content);
break;

default:
throw new ArgumentException($"Unexpected message type: {message?.GetType()}");
}
}

return chatMessages;
}
}
122 changes: 122 additions & 0 deletions dotnet/src/AutoGen.Anthropic/AnthropicClient.cs
Original file line number Diff line number Diff line change
@@ -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<ChatCompletionResponse> CreateChatCompletionsAsync(ChatCompletionRequest chatCompletionRequest,
CancellationToken cancellationToken)
{
var httpResponseMessage = await SendRequestAsync(chatCompletionRequest, cancellationToken);
var responseStream = await httpResponseMessage.Content.ReadAsStreamAsync();

if (httpResponseMessage.IsSuccessStatusCode)
return await DeserializeResponseAsync<ChatCompletionResponse>(responseStream, cancellationToken);

ErrorResponse res = await DeserializeResponseAsync<ErrorResponse>(responseStream, cancellationToken);
throw new Exception(res.Error?.Message);
}

public async IAsyncEnumerable<ChatCompletionResponse> 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<ChatCompletionResponse>(
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<ErrorResponse>(
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<HttpResponseMessage> SendRequestAsync<T>(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<T> DeserializeResponseAsync<T>(Stream responseStream, CancellationToken cancellationToken)
{
return await JsonSerializer.DeserializeAsync<T>(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;
}
}
}
22 changes: 22 additions & 0 deletions dotnet/src/AutoGen.Anthropic/AutoGen.Anthropic.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>AutoGen.Anthropic</RootNamespace>
</PropertyGroup>

<Import Project="$(RepoRoot)/nuget/nuget-package.props" />

<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>AutoGen.Anthropic</Title>
<Description>
Provide support for consuming Anthropic models in AutoGen
</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AutoGen.Core\AutoGen.Core.csproj" />
</ItemGroup>

</Project>
37 changes: 37 additions & 0 deletions dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs
Original file line number Diff line number Diff line change
@@ -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<ContentBase>
{
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<TextContent>(text, options) ?? throw new InvalidOperationException();
case "image":
return JsonSerializer.Deserialize<ImageContent>(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);
}
}
Loading
Loading