-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce AnthropicClient and AnthropicClientAgent (#2769)
* Reference project Revert "Set up the Agent. Basic Example set up, boilerplate for connector, ran into signing issue." This reverts commit 0afe04f2 End to end working anthropic agent + unit tests Set up the Agent. Basic Example set up, boilerplate for connector, ran into signing issue. * Add pragma warning * - Remove Message type - tabbing fix white space in csproj - Remove redundant inheritance - Edit Anthropic.Tests' rootnamespace - Create AutoGen.Anthropic.Samples * short-cut agent extension method * Pass system message in the constructor and throw if there's system message in Imessages --------- Co-authored-by: luongdavid <[email protected]>
- Loading branch information
1 parent
80b2dba
commit 4fec246
Showing
19 changed files
with
891 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
dotnet/sample/AutoGen.Anthropic.Samples/AnthropicSamples.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
dotnet/sample/AutoGen.Anthropic.Samples/AutoGen.Anthropic.Samples.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
91
dotnet/src/AutoGen.Anthropic/Agent/AnthropicClientAgent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": | ||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
37
dotnet/src/AutoGen.Anthropic/Converters/ContentBaseConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.