docs: work on README

This commit is contained in:
Stevan Freeborn
2024-07-06 08:46:40 -05:00
parent dd380c9253
commit 5c0cad2545
5 changed files with 669 additions and 7 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
"dotnet.defaultSolution": "AnthropicClient.sln", "dotnet.defaultSolution": "AnthropicClient.sln",
"cSpell.words": [ "cSpell.words": [
"Browsable", "Browsable",
"Szalay" "nameof",
"Szalay",
"typeof"
], ],
"dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings" "dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings"
} }
+263 -4
View File
@@ -81,7 +81,7 @@ This library was developed to make using the Anthropic API easier within a .NET
## Usage ## Usage
The primary use case for working with the Anthropic API is to create a message in response to a request that includes one or more other messages. The created message can then be received either as a complete response or a stream of events. This can be used to create a conversation between the caller and the Anthropic's AI models and/or to use Anthropic's AI models to perform a task. The primary use case for working with the Anthropic API is to create a message in response to a request that includes one or more other messages. The created message can then be received either as a complete response or a stream of events. This can be used to create a conversation between the caller and Anthropic's AI models and/or to use Anthropic's AI models to perform a task.
> [!NOTE] > [!NOTE]
> The following examples assume that you have already created an instance of the `AnthropicApiClient` class named `client`. > The following examples assume that you have already created an instance of the `AnthropicApiClient` class named `client`.
@@ -93,6 +93,7 @@ The `AnthropicApiClient` exposes a single method named `CreateMessageAsync` that
#### Non-Streaming #### Non-Streaming
```csharp ```csharp
using AnthropicClient;
using AnthropicClient.Models; using AnthropicClient.Models;
var response = await client.CreateMessageAsync(new MessageRequest( var response = await client.CreateMessageAsync(new MessageRequest(
@@ -110,6 +111,7 @@ if (response.IsSuccess is false)
Console.WriteLine($"Failed to create message"); Console.WriteLine($"Failed to create message");
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type); Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message); Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
} }
foreach (var content in response.Value.Content) foreach (var content in response.Value.Content)
@@ -125,11 +127,12 @@ foreach (var content in response.Value.Content)
#### Streaming #### Streaming
Anthropic uses Server-Sent Events (SSE) to stream messages. The possible events and the format of those events are documented in the [Anthropic API Documentation](https://docs.anthropic.com/en/api/messages-streaming). This library provides a way to consume them deserialized into strongly-typed C# objects that are returned in an `IAsyncEnumerable` collection. Anthropic uses Server-Sent Events (SSE) to stream messages. The possible events and the format of those events are documented in the [Anthropic API Documentation](https://docs.anthropic.com/en/api/messages-streaming). This library provides a way to consume them after they have been deserialized into strongly-typed C# objects that are returned in an `IAsyncEnumerable` collection.
This allows you to consume the events as they are received and process them in the way that best fits your use case. The following example demonstrates how to consume the streamed events and build up the complete text response from the model. This allows you to consume the events as they are received and process them in the way that best fits your use case. The following example demonstrates how to consume the streamed events and build up the complete text response from the model.
```csharp ```csharp
using AnthropicClient;
using AnthropicClient.Models; using AnthropicClient.Models;
var events = client.CreateMessageAsync(new StreamMessageRequest( var events = client.CreateMessageAsync(new StreamMessageRequest(
@@ -164,9 +167,10 @@ Console.WriteLine(msgBuilder.ToString());
##### Message Complete Event ##### Message Complete Event
This library also provides a custom `message_complete` event that is yielded when all the message's events have been received. This event is not part of Anthropic's SSE events but is provided to allow for easier consumption of the entire message response if desired and make it easier to implement built in tool calling. This library also provides a custom `message_complete` event that is yielded when all the message's events have been received. This event is not part of Anthropic's SSE events but is provided to allow for easier consumption of the entire message response if desired and make it easier to implement built-in tool calling.
```csharp ```csharp
using AnthropicClient;
using AnthropicClient.Models; using AnthropicClient.Models;
var events = client.CreateMessageAsync(new StreamMessageRequest( var events = client.CreateMessageAsync(new StreamMessageRequest(
@@ -199,12 +203,267 @@ var textContent = response?.Content
Console.WriteLine(textContent); Console.WriteLine(textContent);
``` ```
### Tool Calling ### Tool Use
Anthropic's models support the use of tools to perform tasks. This allows the models to interact with external client-side tools that can perform actions the models cannot do natively. This gives you the ability to further extend the model's abilities with your own custom tools. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use). This library aims to make using tools convenient by allowing you to create, provide, and call tools from within your application by leveraging the reflection capabilities of C#.
> [!NOTE]
> All tools are user provided. The models do no not have access to any built-in server-side tools.
#### Create a tool #### Create a tool
You can create a tool in 4 different ways and then provide that tool when creating a message.
1. Create a tool from a class
1. Create a tool from a static method
1. Create a tool from an instance method
1. Create a tool from a delegate
##### Create a tool from a class
When creating a tool from a class the class must implement the `ITool` interface.
```csharp
using AnthropicClient.Models;
class GetWeatherTool : ITool
{
public string Name => "Get Weather";
public string Description => "Get the weather for a location in the specified units";
public MethodInfo Function => typeof(GetWeatherTool).GetMethod(nameof(GetWeather))!;
public static string GetWeather(string location, string units)
{
return $"The weather in {location} is 72 degrees {units}";
}
}
var getWeatherTool = Tool.CreateFromClass<GetWeatherTool>();
```
##### Create a tool from a static method
When creating a tool from a static method the method must be public and static.
```csharp
using AnthropicClient.Models;
class GetWeatherTool
{
public static string GetWeather(string location)
{
return $"The weather in {location} is 72 degrees Fahrenheit";
}
}
var getWeatherTool = Tool.CreateFromStaticMethod(
"Get Weather",
"Get the weather for a location in the specified units",
typeof(GetWeatherTool),
nameof(GetWeatherTool.GetWeather)
);
```
##### Create a tool from an instance method
When creating a tool from an instance method the method must be public and non-static.
```csharp
using AnthropicClient.Models;
class GetWeatherTool
{
public string GetWeather(string location)
{
return $"The weather in {location} is 72 degrees Fahrenheit";
}
}
var toolInstance = new GetWeatherTool();
var getWeatherTool = Tool.CreateFromInstanceMethod(
"Get Weather",
"Get the weather for a location in the specified units",
toolInstance,
nameof(toolInstance.GetWeather)
);
```
##### Create a tool from a delegate
When creating a tool from a delegate the delegate must be a `Func<TResult>`, `Func<T, TResult>`, or `Func<T1, T2, TResult>`. If you need to create a tool from a delegate that takes more than 2 parameters you should create a complex type and pass that as the parameter.
```csharp
using AnthropicClient.Models;
var tool = (string location, string units) => $"The weather in {location} is 72 degrees {units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
```
##### Function Parameter Attribute
When you create a tool from one of the methods above and send it to Anthropic in your request a JSON representation of the tool is provided in the message. This JSON representation includes the name, description, and input schema of the tool. This information is used by Anthropic's models to discern if and when it should use a tool.
This library provides a `FunctionParameterAttribute` that can be used to provide additional information about the parameters of the tool. This information is used to provide a more detailed input schema for the tool.
```csharp
using AnthropicClient.Models;
var tool = (
[FunctionParameter(description: "The location of the weather being got", name: "Location", required: true)]
string location,
string units
) => $"The weather in {location} is 72 degrees {units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
```
##### Function Property Attribute
This library also provides a `FunctionPropertyAttribute` that can be used to provide additional information about the members of complex types used as parameters in the tool. This information is used to provide a more detailed input schema for the tool.
```csharp
using AnthropicClient.Models;
class GetWeatherInput
{
[FunctionProperty(
description: "The location of the weather being got",
required: true
)]
public string Location { get; } = string.Empty;
[FunctionProperty(
description: "The units to get the weather in",
required: false,
defaultValue: "Fahrenheit",
possibleValues: ["Fahrenheit", "Celsius"]
)]
public string Units { get; } = "Fahrenheit";
}
var tool = (GetWeatherInput input) => $"The weather in {input.Location} is 72 degrees {input.Units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
```
#### Call a tool #### Call a tool
It is important to remember that while Anthropic's models do support tool use they don't actually have access to any built-in server-side tools. All tools are user provided. This means that while Anthropic's models can respond to a request to create a message with a request to use a tool that is all it is - a request. It is still up to the client to handle the tool request by calling the tool with the input provided by the model and then providing the result of that call back to the model.
This library aims to make this process convenient by allowing you to simply provide the tools you want Anthropic's models to consider for use when creating a message, receive the response, check if the response contains a tool call, and if it does invoke the tool to get the result.
```csharp
using AnthropicClient;
using AnthropicClient.Models;
List<Message> messages = [
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
];
List<Tool> tools = [Tool.CreateFromClass<GetWeatherTool>()];
var response = await client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
messages,
tools: tools
));
if (response.IsSuccess is false)
{
Console.WriteLine($"Failed to create message");
Console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
Console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
messages.Add(new(MessageRole.Assistant, [content]));
switch (content)
{
case TextContent textContent:
Console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
Console.WriteLine(toolUseContent.Name);
break;
}
}
if (response.Value.ToolCall is not null)
{
var toolCallResult = await response.Value.ToolCall.InvokeAsync<string>();
string toolResultContent;
if (toolCallResult.IsSuccess && toolCallResult.Value is not null)
{
Console.WriteLine(toolCallResult.Value);
toolResultContent = toolCallResult.Value;
}
else
{
Console.WriteLine(toolCallResult.Error.Message);
toolResultContent = toolCallResult.Error.Message;
}
messages.Add(
new(
MessageRole.User,
[
new ToolResultContent(
response.Value.ToolCall.ToolUse.Id,
toolResultContent
)
]
)
);
}
var finalResponse = await client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
messages,
tools: tools
));
if (finalResponse.IsSuccess is false)
{
Console.WriteLine($"Failed to create message");
Console.WriteLine($"Error Type: {0}", finalResponse.Error.Error.Type);
Console.WriteLine($"Error Message: {0}", finalResponse.Error.Error.Message);
return;
}
foreach (var content in finalResponse.Value.Content)
{
switch (content)
{
case TextContent textContent:
Console.WriteLine(textContent.Text);
break;
}
}
```
The `InvokeAsync` method does accept a generic type parameter that can be used to specify the type of the `Value` property of the `ToolCallResult`. If it is not specified it will be an `object`.
#### Call a tool in streamed message #### Call a tool in streamed message
+1 -1
View File
@@ -107,7 +107,7 @@ public class Tool
ArgumentValidator.ThrowIfNullOrWhitespace(tool.Description, nameof(tool.Description)); ArgumentValidator.ThrowIfNullOrWhitespace(tool.Description, nameof(tool.Description));
ArgumentValidator.ThrowIfNull(tool.Function, nameof(tool.Function)); ArgumentValidator.ThrowIfNull(tool.Function, nameof(tool.Function));
return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function)); return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function, tool));
} }
/// <summary> /// <summary>
@@ -1,5 +1,7 @@
#pragma warning disable xUnit1004 #pragma warning disable xUnit1004
using System.Reflection;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace AnthropicClient.Tests.Examples; namespace AnthropicClient.Tests.Examples;
@@ -27,6 +29,7 @@ public class Examples(ConfigurationFixture config, ITestOutputHelper console) :
_console.WriteLine($"Failed to create message"); _console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type); _console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message); _console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
} }
foreach (var content in response.Value.Content) foreach (var content in response.Value.Content)
@@ -105,4 +108,402 @@ public class Examples(ConfigurationFixture config, ITestOutputHelper console) :
_console.WriteLine(textContent); _console.WriteLine(textContent);
} }
[Example]
public async Task CreateAToolFromAClass()
{
var getWeatherTool = Tool.CreateFromClass<GetWeatherTool>();
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAToolFromAStaticMethod()
{
var getWeatherTool = Tool.CreateFromStaticMethod(
"Get Weather",
"Get the weather for a location in the specified units",
typeof(GetWeatherTool),
nameof(GetWeatherTool.GetWeather)
);
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAToolFromInstanceMethod()
{
var toolInstance = new GetWeatherTool();
var getWeatherTool = Tool.CreateFromInstanceMethod(
"Get Weather",
"Get the weather for a location in the specified units",
toolInstance,
nameof(toolInstance.GetWeather)
);
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAToolFromDelegate()
{
var tool = (string location, string units) => $"The weather in {location} is 72 degrees {units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAToolFromDelegateWithParameterAttributes()
{
var tool = (
[FunctionParameter(description: "The location of the weather being got", name: "Location", required: true)]
string location,
string units
) => $"The weather in {location} is 72 degrees {units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAToolFromDelegateWithInputClass()
{
var tool = (GetWeatherInput input) => $"The weather in {input.Location} is 72 degrees {input.Units}";
var getWeatherTool = Tool.CreateFromFunction(
"Get Weather",
"Get the weather for a location in the specified units",
tool
);
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
[
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
],
tools: [getWeatherTool]
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
}
[Example]
public async Task CreateAMessageProvideAToolAndCallTool()
{
List<Message> messages = [
new(
MessageRole.User,
[new TextContent("What is the weather in New York?")]
)
];
List<Tool> tools = [Tool.CreateFromClass<GetWeatherTool>()];
var response = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
messages,
tools: tools
));
if (response.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", response.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", response.Error.Error.Message);
return;
}
foreach (var content in response.Value.Content)
{
messages.Add(new(MessageRole.Assistant, [content]));
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
case ToolUseContent toolUseContent:
_console.WriteLine(toolUseContent.Name);
break;
}
}
if (response.Value.ToolCall is not null)
{
var toolCallResult = await response.Value.ToolCall.InvokeAsync<string>();
string toolResultContent;
if (toolCallResult.IsSuccess && toolCallResult.Value is not null)
{
_console.WriteLine(toolCallResult.Value);
toolResultContent = toolCallResult.Value;
}
else
{
_console.WriteLine(toolCallResult.Error.Message);
toolResultContent = toolCallResult.Error.Message;
}
messages.Add(
new(
MessageRole.User,
[
new ToolResultContent(
response.Value.ToolCall.ToolUse.Id,
toolResultContent
)
]
)
);
}
var finalResponse = await _client.CreateMessageAsync(new MessageRequest(
AnthropicModels.Claude3Haiku,
messages,
tools: tools
));
if (finalResponse.IsSuccess is false)
{
_console.WriteLine($"Failed to create message");
_console.WriteLine($"Error Type: {0}", finalResponse.Error.Error.Type);
_console.WriteLine($"Error Message: {0}", finalResponse.Error.Error.Message);
return;
}
foreach (var content in finalResponse.Value.Content)
{
switch (content)
{
case TextContent textContent:
_console.WriteLine(textContent.Text);
break;
}
}
}
}
class GetWeatherTool : ITool
{
public string Name => "Get Weather";
public string Description => "Get the weather for a location in the specified units";
public MethodInfo Function => typeof(GetWeatherTool).GetMethod(nameof(GetWeather))!;
public string GetWeather(string location, string units)
{
return $"The weather in {location} is 72 degrees {units}";
}
public string GetWeatherByInput(GetWeatherInput input)
{
return $"The weather in {input.Location} is 72 degrees {input.Units}";
}
public static string GetWeatherStatically(string location)
{
return $"The weather in {location} is 72 degrees Fahrenheit";
}
}
class GetWeatherInput
{
[FunctionProperty(
description: "The location of the weather being got",
required: true
)]
public string Location { get; } = string.Empty;
[FunctionProperty(
description: "The units to get the weather in",
required: false,
defaultValue: "Fahrenheit",
possibleValues: ["Fahrenheit", "Celsius"]
)]
public string Units { get; } = "Fahrenheit";
} }
@@ -160,7 +160,7 @@ public class ToolTests : SerializationTest
public void CreateFromInstanceMethod_WhenCalled_ItShouldReturnTool() public void CreateFromInstanceMethod_WhenCalled_ItShouldReturnTool()
{ {
var instance = new TestClass(); var instance = new TestClass();
var tool = Tool.CreateFromInstanceMethod("test name", "description", instance, nameof(TestClass.TestInstanceMethod)); var tool = Tool.CreateFromInstanceMethod("test name", "description", instance, nameof(instance.TestInstanceMethod));
var expectedSchema = new JsonObject() var expectedSchema = new JsonObject()
{ {