diff --git a/.gitattributes b/.gitattributes
index c139e44b4dc..513c7ecbf03 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,91 @@
+# Source code
+*.bash text eol=lf
+*.bat text eol=crlf
+*.cmd text eol=crlf
+*.coffee text
+*.css text diff=css eol=lf
+*.htm text diff=html eol=lf
+*.html text diff=html eol=lf
+*.inc text
+*.ini text
+*.js text
+*.json text eol=lf
+*.jsx text
+*.less text
+*.ls text
+*.map text -diff
+*.od text
+*.onlydata text
+*.php text diff=php
+*.pl text
+*.ps1 text eol=crlf
+*.py text diff=python eol=lf
+*.rb text diff=ruby eol=lf
+*.sass text
+*.scm text
+*.scss text diff=css
+*.sh text eol=lf
+.husky/* text eol=lf
+*.sql text
+*.styl text
+*.tag text
+*.ts text
+*.tsx text
+*.xml text
+*.xhtml text diff=html
+
+# Docker
+Dockerfile text eol=lf
+
+# Documentation
+*.ipynb text
+*.markdown text diff=markdown eol=lf
+*.md text diff=markdown eol=lf
+*.mdwn text diff=markdown eol=lf
+*.mdown text diff=markdown eol=lf
+*.mkd text diff=markdown eol=lf
+*.mkdn text diff=markdown eol=lf
+*.mdtxt text eol=lf
+*.mdtext text eol=lf
+*.txt text eol=lf
+AUTHORS text eol=lf
+CHANGELOG text eol=lf
+CHANGES text eol=lf
+CONTRIBUTING text eol=lf
+COPYING text eol=lf
+copyright text eol=lf
+*COPYRIGHT* text eol=lf
+INSTALL text eol=lf
+license text eol=lf
+LICENSE text eol=lf
+NEWS text eol=lf
+readme text eol=lf
+*README* text eol=lf
+TODO text
+
+# Configs
+*.cnf text eol=lf
+*.conf text eol=lf
+*.config text eol=lf
+.editorconfig text
+.env text eol=lf
+.gitattributes text eol=lf
+.gitconfig text eol=lf
+.htaccess text
+*.lock text -diff
+package.json text eol=lf
+package-lock.json text eol=lf -diff
+pnpm-lock.yaml text eol=lf -diff
+.prettierrc text
+yarn.lock text -diff
+*.toml text eol=lf
+*.yaml text eol=lf
+*.yml text eol=lf
+browserslist text
+Makefile text eol=lf
+makefile text eol=lf
+
+# Images
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/contrib-tests.yml b/.github/workflows/contrib-tests.yml
index 38fab877402..98a47e1e510 100644
--- a/.github/workflows/contrib-tests.yml
+++ b/.github/workflows/contrib-tests.yml
@@ -418,9 +418,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-2019]
python-version: ["3.11"]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install packages and dependencies for all tests
diff --git a/autogen/agentchat/contrib/vectordb/pgvectordb.py b/autogen/agentchat/contrib/vectordb/pgvectordb.py
index b5db55f7eb1..38507cb7998 100644
--- a/autogen/agentchat/contrib/vectordb/pgvectordb.py
+++ b/autogen/agentchat/contrib/vectordb/pgvectordb.py
@@ -1,7 +1,7 @@
import os
import re
import urllib.parse
-from typing import Callable, List
+from typing import Callable, List, Optional, Union
import numpy as np
from sentence_transformers import SentenceTransformer
@@ -231,7 +231,14 @@ def table_exists(self, table_name: str) -> bool:
exists = cursor.fetchone()[0]
return exists
- def get(self, ids=None, include=None, where=None, limit=None, offset=None) -> List[Document]:
+ def get(
+ self,
+ ids: Optional[str] = None,
+ include: Optional[str] = None,
+ where: Optional[str] = None,
+ limit: Optional[Union[int, str]] = None,
+ offset: Optional[Union[int, str]] = None,
+ ) -> List[Document]:
"""
Retrieve documents from the collection.
@@ -272,7 +279,6 @@ def get(self, ids=None, include=None, where=None, limit=None, offset=None) -> Li
# Construct the full query
query = f"{select_clause} {from_clause} {where_clause} {limit_clause} {offset_clause}"
-
retrieved_documents = []
try:
# Execute the query with the appropriate values
@@ -380,11 +386,11 @@ def inner_product_distance(arr1: List[float], arr2: List[float]) -> float:
def query(
self,
query_texts: List[str],
- collection_name: str = None,
- n_results: int = 10,
- distance_type: str = "euclidean",
- distance_threshold: float = -1,
- include_embedding: bool = False,
+ collection_name: Optional[str] = None,
+ n_results: Optional[int] = 10,
+ distance_type: Optional[str] = "euclidean",
+ distance_threshold: Optional[float] = -1,
+ include_embedding: Optional[bool] = False,
) -> QueryResults:
"""
Query documents in the collection.
@@ -450,7 +456,7 @@ def query(
return results
@staticmethod
- def convert_string_to_array(array_string) -> List[float]:
+ def convert_string_to_array(array_string: str) -> List[float]:
"""
Convert a string representation of an array to a list of floats.
@@ -467,7 +473,7 @@ def convert_string_to_array(array_string) -> List[float]:
array = [float(num) for num in array_string.split()]
return array
- def modify(self, metadata, collection_name: str = None) -> None:
+ def modify(self, metadata, collection_name: Optional[str] = None) -> None:
"""
Modify metadata for the collection.
@@ -486,7 +492,7 @@ def modify(self, metadata, collection_name: str = None) -> None:
)
cursor.close()
- def delete(self, ids: List[ItemID], collection_name: str = None) -> None:
+ def delete(self, ids: List[ItemID], collection_name: Optional[str] = None) -> None:
"""
Delete documents from the collection.
@@ -504,7 +510,7 @@ def delete(self, ids: List[ItemID], collection_name: str = None) -> None:
cursor.execute(f"DELETE FROM {self.name} WHERE id IN ({id_placeholders});", ids)
cursor.close()
- def delete_collection(self, collection_name: str = None) -> None:
+ def delete_collection(self, collection_name: Optional[str] = None) -> None:
"""
Delete the entire collection.
@@ -520,7 +526,7 @@ def delete_collection(self, collection_name: str = None) -> None:
cursor.execute(f"DROP TABLE IF EXISTS {self.name}")
cursor.close()
- def create_collection(self, collection_name: str = None) -> None:
+ def create_collection(self, collection_name: Optional[str] = None) -> None:
"""
Create a new collection.
@@ -557,16 +563,17 @@ class PGVectorDB(VectorDB):
def __init__(
self,
*,
- connection_string: str = None,
- host: str = None,
- port: int = None,
- dbname: str = None,
- username: str = None,
- password: str = None,
- connect_timeout: int = 10,
+ conn: Optional[psycopg.Connection] = None,
+ connection_string: Optional[str] = None,
+ host: Optional[str] = None,
+ port: Optional[Union[int, str]] = None,
+ dbname: Optional[str] = None,
+ username: Optional[str] = None,
+ password: Optional[str] = None,
+ connect_timeout: Optional[int] = 10,
embedding_function: Callable = None,
- metadata: dict = None,
- model_name: str = "all-MiniLM-L6-v2",
+ metadata: Optional[dict] = None,
+ model_name: Optional[str] = "all-MiniLM-L6-v2",
) -> None:
"""
Initialize the vector database.
@@ -574,6 +581,9 @@ def __init__(
Note: connection_string or host + port + dbname must be specified
Args:
+ conn: psycopg.Connection | A customer connection object to connect to the database.
+ A connection object may include additional key/values:
+ https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
connection_string: "postgresql://username:password@hostname:port/database" | The PGVector connection string. Default is None.
host: str | The host to connect to. Default is None.
port: int | The port to connect to. Default is None.
@@ -593,46 +603,108 @@ def __init__(
Returns:
None
"""
+ self.client = self.establish_connection(
+ conn=conn,
+ connection_string=connection_string,
+ host=host,
+ port=port,
+ dbname=dbname,
+ username=username,
+ password=password,
+ connect_timeout=connect_timeout,
+ )
+ self.model_name = model_name
try:
- if connection_string:
+ self.embedding_function = (
+ SentenceTransformer(self.model_name) if embedding_function is None else embedding_function
+ )
+ except Exception as e:
+ logger.error(
+ f"Validate the model name entered: {self.model_name} "
+ f"from https://huggingface.co/models?library=sentence-transformers\nError: {e}"
+ )
+ raise e
+ self.metadata = metadata
+ register_vector(self.client)
+ self.active_collection = None
+
+ def establish_connection(
+ self,
+ conn: Optional[psycopg.Connection] = None,
+ connection_string: Optional[str] = None,
+ host: Optional[str] = None,
+ port: Optional[Union[int, str]] = None,
+ dbname: Optional[str] = None,
+ username: Optional[str] = None,
+ password: Optional[str] = None,
+ connect_timeout: Optional[int] = 10,
+ ) -> psycopg.Connection:
+ """
+ Establishes a connection to a PostgreSQL database using psycopg.
+
+ Args:
+ conn: An existing psycopg connection object. If provided, this connection will be used.
+ connection_string: A string containing the connection information. If provided, a new connection will be established using this string.
+ host: The hostname of the PostgreSQL server. Used if connection_string is not provided.
+ port: The port number to connect to at the server host. Used if connection_string is not provided.
+ dbname: The database name. Used if connection_string is not provided.
+ username: The username to connect as. Used if connection_string is not provided.
+ password: The user's password. Used if connection_string is not provided.
+ connect_timeout: Maximum wait for connection, in seconds. The default is 10 seconds.
+
+ Returns:
+ A psycopg.Connection object representing the established connection.
+
+ Raises:
+ PermissionError if no credentials are supplied
+ psycopg.Error: If an error occurs while trying to connect to the database.
+ """
+ try:
+ if conn:
+ self.client = conn
+ elif connection_string:
parsed_connection = urllib.parse.urlparse(connection_string)
encoded_username = urllib.parse.quote(parsed_connection.username, safe="")
encoded_password = urllib.parse.quote(parsed_connection.password, safe="")
+ encoded_password = f":{encoded_password}@"
encoded_host = urllib.parse.quote(parsed_connection.hostname, safe="")
+ encoded_port = f":{parsed_connection.port}"
encoded_database = urllib.parse.quote(parsed_connection.path[1:], safe="")
connection_string_encoded = (
- f"{parsed_connection.scheme}://{encoded_username}:{encoded_password}"
- f"@{encoded_host}:{parsed_connection.port}/{encoded_database}"
+ f"{parsed_connection.scheme}://{encoded_username}{encoded_password}"
+ f"{encoded_host}{encoded_port}/{encoded_database}"
)
self.client = psycopg.connect(conninfo=connection_string_encoded, autocommit=True)
- elif host and port and dbname:
+ elif host:
+ connection_string = ""
+ if host:
+ encoded_host = urllib.parse.quote(host, safe="")
+ connection_string += f"host={encoded_host} "
+ if port:
+ connection_string += f"port={port} "
+ if dbname:
+ encoded_database = urllib.parse.quote(dbname, safe="")
+ connection_string += f"dbname={encoded_database} "
+ if username:
+ encoded_username = urllib.parse.quote(username, safe="")
+ connection_string += f"user={encoded_username} "
+ if password:
+ encoded_password = urllib.parse.quote(password, safe="")
+ connection_string += f"password={encoded_password} "
+
self.client = psycopg.connect(
- host=host,
- port=port,
- dbname=dbname,
- username=username,
- password=password,
+ conninfo=connection_string,
connect_timeout=connect_timeout,
autocommit=True,
)
+ else:
+ logger.error("Credentials were not supplied...")
+ raise PermissionError
+ self.client.execute("CREATE EXTENSION IF NOT EXISTS vector")
except psycopg.Error as e:
logger.error("Error connecting to the database: ", e)
raise e
- self.model_name = model_name
- try:
- self.embedding_function = (
- SentenceTransformer(self.model_name) if embedding_function is None else embedding_function
- )
- except Exception as e:
- logger.error(
- f"Validate the model name entered: {self.model_name} "
- f"from https://huggingface.co/models?library=sentence-transformers\nError: {e}"
- )
- raise e
- self.metadata = metadata
- self.client.execute("CREATE EXTENSION IF NOT EXISTS vector")
- register_vector(self.client)
- self.active_collection = None
+ return self.client
def create_collection(
self, collection_name: str, overwrite: bool = False, get_or_create: bool = True
diff --git a/autogen/function_utils.py b/autogen/function_utils.py
index dd225fd4719..6b9b6f5b129 100644
--- a/autogen/function_utils.py
+++ b/autogen/function_utils.py
@@ -353,4 +353,4 @@ def serialize_to_str(x: Any) -> str:
elif isinstance(x, BaseModel):
return model_dump_json(x)
else:
- return json.dumps(x)
+ return json.dumps(x, ensure_ascii=False)
diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln
index be40e7b61b6..de2549cae13 100644
--- a/dotnet/AutoGen.sln
+++ b/dotnet/AutoGen.sln
@@ -44,8 +44,15 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Ollama.Tests", "test\AutoGen.Ollama.Tests\AutoGen.Ollama.Tests.csproj", "{03E31CAA-3728-48D3-B936-9F11CF6C18FE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.Ollama.Sample", "sample\AutoGen.Ollama.Sample\AutoGen.Ollama.Sample.csproj", "{93AA4D0D-6EE4-44D5-AD77-7F73A3934544}"
+EndProject
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 +135,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 +167,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