Skip to content

Commit

Permalink
Introduce AnthropicClient and AnthropicClientAgent (#2769)
Browse files Browse the repository at this point in the history
* 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 <luongdavid@microsoft.com>
  • Loading branch information
2 people authored and victordibia committed Jul 30, 2024
1 parent fdd6660 commit 3a8628e
Show file tree
Hide file tree
Showing 19 changed files with 891 additions and 0 deletions.
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":
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

0 comments on commit 3a8628e

Please sign in to comment.