From ed97ea95dc4a2f0bfb9fadd284aad6fed1d4b7e1 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Thu, 15 Aug 2024 23:04:45 -0500 Subject: [PATCH 01/26] feat: first take of adding caching support --- .../Json/JsonSerializationOptions.cs | 1 + .../Json/MessageRequestConverter.cs | 19 +++ .../Models/BaseMessageRequest.cs | 156 ++++++++++++++---- src/AnthropicClient/Models/CacheControl.cs | 27 +++ .../Models/CacheControlType.cs | 12 ++ src/AnthropicClient/Models/Content.cs | 18 ++ src/AnthropicClient/Models/ImageContent.cs | 16 ++ src/AnthropicClient/Models/TextContent.cs | 14 ++ src/AnthropicClient/Models/Tool.cs | 64 +++++-- src/AnthropicClient/Models/Usage.cs | 12 ++ 10 files changed, 293 insertions(+), 46 deletions(-) create mode 100644 src/AnthropicClient/Json/MessageRequestConverter.cs create mode 100644 src/AnthropicClient/Models/CacheControl.cs create mode 100644 src/AnthropicClient/Models/CacheControlType.cs diff --git a/src/AnthropicClient/Json/JsonSerializationOptions.cs b/src/AnthropicClient/Json/JsonSerializationOptions.cs index 6e9dece..a5a9ce8 100644 --- a/src/AnthropicClient/Json/JsonSerializationOptions.cs +++ b/src/AnthropicClient/Json/JsonSerializationOptions.cs @@ -17,6 +17,7 @@ static class JsonSerializationOptions new EventDataConverter(), new ContentDeltaConverter(), new JsonStringEnumConverter(), + new MessageRequestConverter(), }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; diff --git a/src/AnthropicClient/Json/MessageRequestConverter.cs b/src/AnthropicClient/Json/MessageRequestConverter.cs new file mode 100644 index 0000000..4e5e538 --- /dev/null +++ b/src/AnthropicClient/Json/MessageRequestConverter.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +using AnthropicClient.Models; + +namespace AnthropicClient.Json; + +class MessageRequestConverter : JsonConverter +{ + public override MessageRequest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(ref reader, options)!; + } + + public override void Write(Utf8JsonWriter writer, MessageRequest value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/BaseMessageRequest.cs b/src/AnthropicClient/Models/BaseMessageRequest.cs index a46dce6..7765310 100644 --- a/src/AnthropicClient/Models/BaseMessageRequest.cs +++ b/src/AnthropicClient/Models/BaseMessageRequest.cs @@ -19,6 +19,11 @@ public abstract class BaseMessageRequest /// public string? System { get; init; } = null; + /// + /// Gets the messages to send to the model. + /// + public List? SystemMessages { get; init; } = null; + /// /// Gets the messages to send to the model. /// @@ -74,41 +79,21 @@ public abstract class BaseMessageRequest [JsonConstructor] internal BaseMessageRequest() { } - - /// - /// Initializes a new instance of the class. - /// - /// The model ID to use for the request. - /// The messages to send to the model. - /// The maximum number of tokens to generate. - /// The system ID to use for the request. - /// The metadata to include with the request. - /// The temperature to use for the request. - /// The top-K value to use for the request. - /// The top-P value to use for the request. - /// The tool choice mode to use for the request. - /// The tools to use for the request. - /// A value indicating whether the message should be streamed. - /// The prompt stop sequences. - /// Thrown when the model ID is invalid. - /// Thrown when the model or messages is null. - /// Thrown when the messages contain no messages. - /// Thrown when the max tokens is less than one. - /// Thrown when the temperature is less than zero or greater than one. - /// A new instance of the class. - protected BaseMessageRequest( + + private BaseMessageRequest( string model, List messages, - int maxTokens = 1024, - string? system = null, - Dictionary? metadata = null, - decimal temperature = 0.0m, - int? topK = null, - decimal? topP = null, - ToolChoice? toolChoice = null, - List? tools = null, - bool stream = false, - List? stopSequences = null + int maxTokens, + string? system, + List? systemMessages, + Dictionary? metadata, + decimal temperature, + int? topK, + decimal? topP, + ToolChoice? toolChoice, + List? tools, + bool stream, + List? stopSequences ) { ArgumentValidator.ThrowIfNull(model, nameof(model)); @@ -138,6 +123,7 @@ public abstract class BaseMessageRequest Messages = messages; MaxTokens = maxTokens; System = system; + SystemMessages = systemMessages; Metadata = metadata; Temperature = temperature; TopK = topK; @@ -147,4 +133,108 @@ public abstract class BaseMessageRequest Stream = stream; StopSequences = stopSequences ?? []; } + + /// + /// Initializes a new instance of the class. + /// + /// The model ID to use for the request. + /// The messages to send to the model. + /// The maximum number of tokens to generate. + /// The system prompt to use for the request. + /// The metadata to include with the request. + /// The temperature to use for the request. + /// The top-K value to use for the request. + /// The top-P value to use for the request. + /// The tool choice mode to use for the request. + /// The tools to use for the request. + /// A value indicating whether the message should be streamed. + /// The prompt stop sequences. + /// Thrown when the model ID is invalid. + /// Thrown when the model or messages is null. + /// Thrown when the messages contain no messages. + /// Thrown when the max tokens is less than one. + /// Thrown when the temperature is less than zero or greater than one. + /// A new instance of the class. + protected BaseMessageRequest( + string model, + List messages, + int maxTokens = 1024, + string? system = null, + Dictionary? metadata = null, + decimal temperature = 0.0m, + int? topK = null, + decimal? topP = null, + ToolChoice? toolChoice = null, + List? tools = null, + bool stream = false, + List? stopSequences = null + ) : this( + model, + messages, + maxTokens, + system, + null, + metadata, + temperature, + topK, + topP, + toolChoice, + tools, + stream, + stopSequences + ) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The model ID to use for the request. + /// The messages to send to the model. + /// The maximum number of tokens to generate. + /// The system messages to send to the model to be used as the system prompt. + /// The metadata to include with the request. + /// The temperature to use for the request. + /// The top-K value to use for the request. + /// The top-P value to use for the request. + /// The tool choice mode to use for the request. + /// The tools to use for the request. + /// A value indicating whether the message should be streamed. + /// The prompt stop sequences. + /// Thrown when the model ID is invalid. + /// Thrown when the model or messages is null. + /// Thrown when the messages contain no messages. + /// Thrown when the max tokens is less than one. + /// Thrown when the temperature is less than zero or greater than one. + /// A new instance of the class. + protected BaseMessageRequest( + string model, + List messages, + int maxTokens = 1024, + List? systemMessages = null, + Dictionary? metadata = null, + decimal temperature = 0.0m, + int? topK = null, + decimal? topP = null, + ToolChoice? toolChoice = null, + List? tools = null, + bool stream = false, + List? stopSequences = null + ) : this( + model, + messages, + maxTokens, + null, + systemMessages, + metadata, + temperature, + topK, + topP, + toolChoice, + tools, + stream, + stopSequences + ) + { + } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/CacheControl.cs b/src/AnthropicClient/Models/CacheControl.cs new file mode 100644 index 0000000..d29dafb --- /dev/null +++ b/src/AnthropicClient/Models/CacheControl.cs @@ -0,0 +1,27 @@ +using AnthropicClient.Utils; + +namespace AnthropicClient.Models; + +/// +/// Represents the cache control to be used for content. +/// +public class CacheControl +{ + /// + /// Gets the type of the cache control. + /// + public string Type { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the cache control. + /// A new instance of the class. + /// Thrown when the type is null or whitespace. + public CacheControl(string type) + { + ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type)); + + Type = type; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/CacheControlType.cs b/src/AnthropicClient/Models/CacheControlType.cs new file mode 100644 index 0000000..aa24f57 --- /dev/null +++ b/src/AnthropicClient/Models/CacheControlType.cs @@ -0,0 +1,12 @@ +namespace AnthropicClient.Models; + +/// +/// Provides constants for cache control types. +/// +public static class CacheControlType +{ + /// + /// The cache control type for an ephemeral cache. + /// + public const string Ephemeral = "ephemeral"; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/Content.cs b/src/AnthropicClient/Models/Content.cs index 2970c3f..946b8c5 100644 --- a/src/AnthropicClient/Models/Content.cs +++ b/src/AnthropicClient/Models/Content.cs @@ -11,6 +11,12 @@ public abstract class Content /// Gets the type of the content. /// public string Type { get; init; } = string.Empty; + + /// + /// Gets the cache control to be used for the content. + /// + [JsonPropertyName("cache_control")] + public CacheControl? CacheControl { get; init; } [JsonConstructor] internal Content() @@ -26,4 +32,16 @@ public abstract class Content { Type = type; } + + /// + /// Initializes a new instance of the class. + /// + /// The type of the content. + /// The cache control to be used for the content. + /// A new instance of the class. + protected Content(string type, CacheControl cacheControl) + { + Type = type; + CacheControl = cacheControl; + } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/ImageContent.cs b/src/AnthropicClient/Models/ImageContent.cs index 6fd5232..20c071c 100644 --- a/src/AnthropicClient/Models/ImageContent.cs +++ b/src/AnthropicClient/Models/ImageContent.cs @@ -33,4 +33,20 @@ public class ImageContent : Content Source = new(mediaType, data); } + + /// + /// Initializes a new instance of the class. + /// + /// The media type of the image. + /// The data of the image. + /// The cache control to be used for the content. + /// A new instance of the class. + /// Thrown when the media type, data, or cache control is null. + public ImageContent(string mediaType, string data, CacheControl cacheControl) : base(ContentType.Image, cacheControl) + { + ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType)); + ArgumentValidator.ThrowIfNull(data, nameof(data)); + + Source = new(mediaType, data); + } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/TextContent.cs b/src/AnthropicClient/Models/TextContent.cs index b68a1a3..01b14ee 100644 --- a/src/AnthropicClient/Models/TextContent.cs +++ b/src/AnthropicClient/Models/TextContent.cs @@ -31,4 +31,18 @@ public class TextContent : Content Text = text; } + + /// + /// Initializes a new instance of the class. + /// + /// The text of the content. + /// The cache control to be used for the content. + /// A new instance of the class. + /// Thrown when the text or cache control is null. + public TextContent(string text, CacheControl cacheControl) : base(ContentType.Text, cacheControl) + { + ArgumentValidator.ThrowIfNull(text, nameof(text)); + + Text = text; + } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/Tool.cs b/src/AnthropicClient/Models/Tool.cs index 0b1ba3f..e861568 100644 --- a/src/AnthropicClient/Models/Tool.cs +++ b/src/AnthropicClient/Models/Tool.cs @@ -55,6 +55,11 @@ public class Tool [JsonIgnore] public AnthropicFunction Function { get; } + /// + /// Gets or sets the cache control to be used for the tool. + /// + public CacheControl? CacheControl { get; set; } + /// /// Gets the display name of the tool. /// @@ -73,7 +78,7 @@ public class Tool DisplayName = string.Empty; } - internal Tool(string name, string description, AnthropicFunction function) + internal Tool(string name, string description, AnthropicFunction function, CacheControl? cacheControl = null) { ArgumentValidator.ThrowIfNullOrWhitespace(name, nameof(name)); ArgumentValidator.ThrowIfNullOrWhitespace(description, nameof(description)); @@ -89,6 +94,7 @@ public class Tool Description = description; Function = function; InputSchema = JsonSchemaGenerator.GenerateInputSchema(function); + CacheControl = cacheControl; } /// @@ -99,7 +105,7 @@ public class Tool /// Thrown when the function of the tool is null. /// The created tool as instance of . /// The implementation of must have a parameterless constructor. - public static Tool CreateFromClass() where T : ITool, new() + public static Tool CreateFromClass(CacheControl? cacheControl = null) where T : ITool, new() { var tool = new T(); @@ -107,7 +113,7 @@ public class Tool ArgumentValidator.ThrowIfNullOrWhitespace(tool.Description, nameof(tool.Description)); ArgumentValidator.ThrowIfNull(tool.Function, nameof(tool.Function)); - return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function, tool)); + return new Tool(tool.Name, tool.Description, new AnthropicFunction(tool.Function, tool), cacheControl); } /// @@ -117,12 +123,19 @@ public class Tool /// The description of the tool. /// The type that contains the method. /// The name of the method. + /// The cache control to be used for the tool. /// Thrown when is null or empty. /// Thrown when is null. /// Thrown when the method is not found in the type. /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. - public static Tool CreateFromStaticMethod(string name, string description, Type type, string methodName) + public static Tool CreateFromStaticMethod( + string name, + string description, + Type type, + string methodName, + CacheControl? cacheControl = null + ) { ArgumentValidator.ThrowIfNullOrWhitespace(methodName, nameof(methodName)); ArgumentValidator.ThrowIfNull(type, nameof(type)); @@ -134,7 +147,7 @@ public class Tool throw new ArgumentException($"Method '{methodName}' not found in type '{type.FullName}'.", nameof(methodName)); } - return new Tool(name, description, new AnthropicFunction(method)); + return new Tool(name, description, new AnthropicFunction(method), cacheControl); } /// @@ -144,12 +157,19 @@ public class Tool /// The description of the tool. /// The instance that contains the method. /// The name of the method. + /// The cache control to be used for the tool. /// Thrown when is null or empty. /// Thrown when is null. /// Thrown when is not found in the type of . /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. - public static Tool CreateFromInstanceMethod(string name, string description, object instance, string methodName) + public static Tool CreateFromInstanceMethod( + string name, + string description, + object instance, + string methodName, + CacheControl? cacheControl = null + ) { ArgumentValidator.ThrowIfNullOrWhitespace(methodName, nameof(methodName)); ArgumentValidator.ThrowIfNull(instance, nameof(instance)); @@ -161,7 +181,7 @@ public class Tool throw new ArgumentException($"Method '{methodName}' not found in type '{instance.GetType().FullName}'.", nameof(methodName)); } - return new Tool(name, description, new AnthropicFunction(method, instance)); + return new Tool(name, description, new AnthropicFunction(method, instance), null); } /// @@ -171,14 +191,20 @@ public class Tool /// The name of the tool. /// The description of the tool. /// The function. + /// The cache control to be used for the tool. /// Thrown when is null. /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. - public static Tool CreateFromFunction(string name, string description, Func func) + public static Tool CreateFromFunction( + string name, + string description, + Func func, + CacheControl? cacheControl = null + ) { ArgumentValidator.ThrowIfNull(func, nameof(func)); - return new Tool(name, description, new AnthropicFunction(func.Method, func.Target)); + return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl); } /// @@ -189,14 +215,20 @@ public class Tool /// The name of the tool. /// The description of the tool. /// The function. + /// The cache control to be used for the tool. /// Thrown when is null. /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. - public static Tool CreateFromFunction(string name, string description, Func func) + public static Tool CreateFromFunction( + string name, + string description, + Func func, + CacheControl? cacheControl = null + ) { ArgumentValidator.ThrowIfNull(func, nameof(func)); - return new Tool(name, description, new AnthropicFunction(func.Method, func.Target)); + return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl); } /// @@ -208,14 +240,20 @@ public class Tool /// The name of the tool. /// The description of the tool. /// The function. + /// The cache control to be used for the tool. /// Thrown when is null. /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. - public static Tool CreateFromFunction(string name, string description, Func func) + public static Tool CreateFromFunction( + string name, + string description, + Func func, + CacheControl? cacheControl = null + ) { ArgumentValidator.ThrowIfNull(func, nameof(func)); - return new Tool(name, description, new AnthropicFunction(func.Method, func.Target)); + return new Tool(name, description, new AnthropicFunction(func.Method, func.Target), cacheControl); } diff --git a/src/AnthropicClient/Models/Usage.cs b/src/AnthropicClient/Models/Usage.cs index 34310ed..09bde25 100644 --- a/src/AnthropicClient/Models/Usage.cs +++ b/src/AnthropicClient/Models/Usage.cs @@ -18,4 +18,16 @@ public class Usage /// [JsonPropertyName("output_tokens")] public int OutputTokens { get; init; } + + /// + /// Gets the number of tokens written to the cache when creating a new entry + /// + [JsonPropertyName("cache_creation_input_tokens")] + public int CacheCreationInputTokens { get; init; } + + /// + /// Gets the number of tokens retrieved from the cache for the request. + /// + [JsonPropertyName("cache_read_input_tokens")] + public int CacheReadInputTokens { get; init; } } \ No newline at end of file From 66008717e1392f6667e2acc0d5016059adc2e491 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:19:47 -0500 Subject: [PATCH 02/26] fix: remove converter --- .../Json/MessageRequestConverter.cs | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/AnthropicClient/Json/MessageRequestConverter.cs diff --git a/src/AnthropicClient/Json/MessageRequestConverter.cs b/src/AnthropicClient/Json/MessageRequestConverter.cs deleted file mode 100644 index 4e5e538..0000000 --- a/src/AnthropicClient/Json/MessageRequestConverter.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -using AnthropicClient.Models; - -namespace AnthropicClient.Json; - -class MessageRequestConverter : JsonConverter -{ - public override MessageRequest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize(ref reader, options)!; - } - - public override void Write(Utf8JsonWriter writer, MessageRequest value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } -} \ No newline at end of file From 900a7a354941f6b5a1357cacbafb64cd8220d918 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:21:43 -0500 Subject: [PATCH 03/26] tests: update tests to account for serialization and deserialization changes with new/modified model properties to support caching --- src/AnthropicClient/AnthropicApiClient.cs | 2 + .../Json/JsonSerializationOptions.cs | 1 - .../Models/BaseMessageRequest.cs | 29 +++++++++++- .../Unit/Models/AnthropicEventTests.cs | 8 +++- .../Unit/Models/MessageDeltaEventDataTests.cs | 28 +++++++++-- .../Unit/Models/MessageRequestTests.cs | 47 ++++++++++++++----- .../Unit/Models/MessageResponseTests.cs | 14 +++++- .../Unit/Models/MessageStartEventDataTests.cs | 4 +- .../Unit/Models/StreamMessageRequestTests.cs | 7 ++- .../Unit/Models/UsageTests.cs | 28 +++++++++-- 10 files changed, 137 insertions(+), 31 deletions(-) diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index ad5d0c8..2c57cdd 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -191,6 +191,8 @@ public class AnthropicApiClient : IAnthropicApiClient { InputTokens = existingUsage.InputTokens + msgDeltaData.Usage.InputTokens, OutputTokens = existingUsage.OutputTokens + msgDeltaData.Usage.OutputTokens, + CacheCreationInputTokens = existingUsage.CacheCreationInputTokens + msgDeltaData.Usage.CacheCreationInputTokens, + CacheReadInputTokens = existingUsage.CacheReadInputTokens + msgDeltaData.Usage.CacheReadInputTokens, }; msgResponse = new MessageResponse() diff --git a/src/AnthropicClient/Json/JsonSerializationOptions.cs b/src/AnthropicClient/Json/JsonSerializationOptions.cs index a5a9ce8..6e9dece 100644 --- a/src/AnthropicClient/Json/JsonSerializationOptions.cs +++ b/src/AnthropicClient/Json/JsonSerializationOptions.cs @@ -17,7 +17,6 @@ static class JsonSerializationOptions new EventDataConverter(), new ContentDeltaConverter(), new JsonStringEnumConverter(), - new MessageRequestConverter(), }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; diff --git a/src/AnthropicClient/Models/BaseMessageRequest.cs b/src/AnthropicClient/Models/BaseMessageRequest.cs index 7765310..fb5413b 100644 --- a/src/AnthropicClient/Models/BaseMessageRequest.cs +++ b/src/AnthropicClient/Models/BaseMessageRequest.cs @@ -15,15 +15,40 @@ public abstract class BaseMessageRequest public string Model { get; init; } = string.Empty; /// - /// Gets the system prompt to use for the request. + /// Gets the system message that will be used as the system prompt if no system messages are provided. /// + [JsonIgnore] public string? System { get; init; } = null; /// - /// Gets the messages to send to the model. + /// Gets the system messages to send to the model to be used as the system prompt. /// + [JsonIgnore] public List? SystemMessages { get; init; } = null; + /// + /// Gets the system prompt that will be used for the request. + /// If will return the system messages if they are provided, otherwise it will return the system message. + /// If neither are provided, it will return null. + /// + [JsonPropertyName("system")] + public List? SystemPrompt => GetSystemPrompt(); + + private List? GetSystemPrompt() + { + if (SystemMessages is not null) + { + return SystemMessages; + } + + if (System is not null) + { + return [new TextContent(System)]; + } + + return null; + } + /// /// Gets the messages to send to the model. /// diff --git a/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs b/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs index 931616b..28da978 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/AnthropicEventTests.cs @@ -13,7 +13,9 @@ public class AnthropicEventTests : SerializationTest ""stop_sequence"": """", ""usage"": { ""input_tokens"": 472, - ""output_tokens"": 2 + ""output_tokens"": 2, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 }, ""content"": [], ""stop_reason"": """" @@ -70,7 +72,9 @@ public class AnthropicEventTests : SerializationTest }, ""usage"": { ""output_tokens"": 89, - ""input_tokens"": 0 + ""input_tokens"": 0, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 }, ""type"": ""message_delta"" } diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs index 0d22412..b164915 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs @@ -10,7 +10,9 @@ public class MessageDeltaEventDataTests : SerializationTest }, ""usage"": { ""input_tokens"": 1, - ""output_tokens"": 1 + ""output_tokens"": 1, + ""cache_creation_input_tokens"": 1, + ""cache_read_input_tokens"": 1 } }"; @@ -18,7 +20,13 @@ public class MessageDeltaEventDataTests : SerializationTest public void Constructor_WhenCalled_ItShouldInitializeProperties() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 }; + var expectedUsage = new Usage + { + InputTokens = 1, + OutputTokens = 1, + CacheCreationInputTokens = 1, + CacheReadInputTokens = 1, + }; var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage); @@ -30,7 +38,13 @@ public class MessageDeltaEventDataTests : SerializationTest public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 }; + var expectedUsage = new Usage + { + InputTokens = 1, + OutputTokens = 1, + CacheCreationInputTokens = 1, + CacheReadInputTokens = 1, + }; var messageDeltaEventData = new MessageDeltaEventData(expectedDelta, expectedUsage); @@ -43,7 +57,13 @@ public class MessageDeltaEventDataTests : SerializationTest public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage { InputTokens = 1, OutputTokens = 1 }; + var expectedUsage = new Usage + { + InputTokens = 1, + OutputTokens = 1, + CacheCreationInputTokens = 1, + CacheReadInputTokens = 1, + }; var messageDeltaEventData = Deserialize(_testJson); diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs index a1ae3b0..bcd3532 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs @@ -4,7 +4,10 @@ public class MessageRequestTests : SerializationTest { private readonly string _testJson = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [ { ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] } ], @@ -21,7 +24,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithAnyToolChoice = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"":[ { ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"":""text"" }] } ], @@ -38,7 +44,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithSpecificToolChoice = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [ { ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] } ], @@ -55,7 +64,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithImageContent = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"":[ { ""role"": ""user"", @@ -80,7 +92,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithUnknownContent = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [{ ""role"": ""user"", ""content"": [{ ""type"": ""unknown"", ""text"": ""text"" }] }], ""max_tokens"": 512, ""metadata"": { ""test"": ""test"" }, @@ -95,7 +110,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithToolUseContent = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [ { ""role"": ""assistant"", @@ -124,7 +142,10 @@ public class MessageRequestTests : SerializationTest private readonly string _testJsonWithToolResultContent = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [ { ""role"": ""assistant"", @@ -307,7 +328,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJson); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); @@ -330,7 +351,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJsonWithAnyToolChoice); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); @@ -352,7 +373,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJsonWithSpecificToolChoice); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); @@ -387,7 +408,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJsonWithImageContent); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); @@ -416,7 +437,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJsonWithToolUseContent); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); @@ -447,7 +468,7 @@ public class MessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJsonWithToolResultContent); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageResponseTests.cs index e2bf053..43a3e02 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageResponseTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageResponseTests.cs @@ -53,7 +53,12 @@ public class MessageResponseTests : SerializationTest ""stop_reason"": ""stop reason"", ""stop_sequence"": ""stop sequence"", ""type"": ""type"", - ""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 }, + ""usage"": { + ""input_tokens"": 1, + ""output_tokens"": 2, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 + }, ""content"": [ { ""text"": ""text content"", ""type"": ""text"" } ] @@ -93,7 +98,12 @@ public class MessageResponseTests : SerializationTest ""stop_reason"": ""stop reason"", ""stop_sequence"": ""stop sequence"", ""type"": ""type"", - ""usage"": { ""input_tokens"": 1, ""output_tokens"": 2 }, + ""usage"": { + ""input_tokens"": 1, + ""output_tokens"": 2, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 + }, ""content"": [ { ""text"": ""text content"", ""type"": ""text"" } ] diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs index 3a550d8..bc6a5fd 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageStartEventDataTests.cs @@ -14,7 +14,9 @@ public class MessageStartEventDataTests : SerializationTest ""stop_sequence"": """", ""usage"": { ""input_tokens"": 25, - ""output_tokens"": 1 + ""output_tokens"": 1, + ""cache_creation_input_tokens"": 0, + ""cache_read_input_tokens"": 0 } } }"; diff --git a/tests/AnthropicClient.Tests/Unit/Models/StreamMessageRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/StreamMessageRequestTests.cs index 16c1274..67ebd1e 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/StreamMessageRequestTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/StreamMessageRequestTests.cs @@ -4,7 +4,10 @@ public class StreamMessageRequestTests : SerializationTest { private readonly string _testJson = @"{ ""model"": ""claude-3-sonnet-20240229"", - ""system"": ""test-system"", + ""system"": [{ + ""type"": ""text"", + ""text"": ""test-system"" + }], ""messages"": [ { ""role"": ""user"", ""content"": [{ ""text"": ""Hello!"", ""type"": ""text"" }] } ], @@ -178,7 +181,7 @@ public class StreamMessageRequestTests : SerializationTest var messageRequest = Deserialize(_testJson); messageRequest!.Model.Should().Be(AnthropicModels.Claude3Sonnet); - messageRequest.System.Should().Be("test-system"); + messageRequest.System.Should().BeNull(); messageRequest.Messages.Should().HaveCount(1); messageRequest.MaxTokens.Should().Be(512); messageRequest.Metadata.Should().HaveCount(1); diff --git a/tests/AnthropicClient.Tests/Unit/Models/UsageTests.cs b/tests/AnthropicClient.Tests/Unit/Models/UsageTests.cs index 345527a..a0ca68f 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/UsageTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/UsageTests.cs @@ -7,26 +7,39 @@ public class UsageTests : SerializationTest { var expectedInputTokens = 1; var expectedOutputTokens = 2; + var expectedCacheCreationInputTokens = 3; + var expectedCacheReadInputTokens = 4; var usage = new Usage { InputTokens = expectedInputTokens, - OutputTokens = expectedOutputTokens + OutputTokens = expectedOutputTokens, + CacheCreationInputTokens = expectedCacheCreationInputTokens, + CacheReadInputTokens = expectedCacheReadInputTokens }; usage.InputTokens.Should().Be(expectedInputTokens); usage.OutputTokens.Should().Be(expectedOutputTokens); + usage.CacheCreationInputTokens.Should().Be(expectedCacheCreationInputTokens); + usage.CacheReadInputTokens.Should().Be(expectedCacheReadInputTokens); } [Fact] public void JsonSerialization_WhenCalled_ItShouldSerializeCorrectly() { - var expectedJson = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }"; + var expectedJson = @"{ + ""input_tokens"": 1, + ""output_tokens"": 2, + ""cache_creation_input_tokens"": 3, + ""cache_read_input_tokens"": 4 + }"; var usage = new Usage { InputTokens = 1, - OutputTokens = 2 + OutputTokens = 2, + CacheCreationInputTokens = 3, + CacheReadInputTokens = 4 }; var actual = Serialize(usage); @@ -37,11 +50,18 @@ public class UsageTests : SerializationTest [Fact] public void JsonDeserialization_WhenCalled_ItShouldDeserializeCorrectly() { - var json = @"{ ""input_tokens"": 1, ""output_tokens"": 2 }"; + var json = @"{ + ""input_tokens"": 1, + ""output_tokens"": 2, + ""cache_creation_input_tokens"": 3, + ""cache_read_input_tokens"": 4 + }"; var usage = Deserialize(json); usage!.InputTokens.Should().Be(1); usage.OutputTokens.Should().Be(2); + usage.CacheCreationInputTokens.Should().Be(3); + usage.CacheReadInputTokens.Should().Be(4); } } \ No newline at end of file From 90e9b9f75eec1488277538fff5f90b817ded3b6b Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:49:50 -0500 Subject: [PATCH 04/26] fix: use additional constructor param with default value instead of overloaded constructor to avoid potentially breaking others code with that would then contain ambigious constructor calls. --- .../Models/BaseMessageRequest.cs | 137 ++++-------------- src/AnthropicClient/Models/MessageRequest.cs | 9 +- .../Models/StreamMessageRequest.cs | 9 +- 3 files changed, 42 insertions(+), 113 deletions(-) diff --git a/src/AnthropicClient/Models/BaseMessageRequest.cs b/src/AnthropicClient/Models/BaseMessageRequest.cs index fb5413b..040f743 100644 --- a/src/AnthropicClient/Models/BaseMessageRequest.cs +++ b/src/AnthropicClient/Models/BaseMessageRequest.cs @@ -14,6 +14,11 @@ public abstract class BaseMessageRequest /// public string Model { get; init; } = string.Empty; + // TODO: I do not like this. I would prefer to have a single property that is a list of TextContent objects. + // This approach was taken to maintain compatibility with the API. As someone could be using the System property + // and changing it to a list of TextContent objects would break their code. + // However if an opportunity arises for a breaking change release, this should be changed. + /// /// Gets the system message that will be used as the system prompt if no system messages are provided. /// @@ -105,12 +110,33 @@ public abstract class BaseMessageRequest [JsonConstructor] internal BaseMessageRequest() { } - private BaseMessageRequest( + /// + /// Initializes a new instance of the class. + /// + /// The model ID to use for the request. + /// The messages to send to the model. + /// The maximum number of tokens to generate. + /// The system prompt to use for the request. + /// The metadata to include with the request. + /// The temperature to use for the request. + /// The top-K value to use for the request. + /// The top-P value to use for the request. + /// The tool choice mode to use for the request. + /// The tools to use for the request. + /// A value indicating whether the message should be streamed. + /// The prompt stop sequences. + /// The system messages to use for the request. + /// Thrown when the model ID is invalid. + /// Thrown when the model or messages is null. + /// Thrown when the messages contain no messages. + /// Thrown when the max tokens is less than one. + /// Thrown when the temperature is less than zero or greater than one. + /// A new instance of the class. + protected BaseMessageRequest( string model, List messages, int maxTokens, string? system, - List? systemMessages, Dictionary? metadata, decimal temperature, int? topK, @@ -118,7 +144,8 @@ public abstract class BaseMessageRequest ToolChoice? toolChoice, List? tools, bool stream, - List? stopSequences + List? stopSequences, + List? systemMessages ) { ArgumentValidator.ThrowIfNull(model, nameof(model)); @@ -158,108 +185,4 @@ public abstract class BaseMessageRequest Stream = stream; StopSequences = stopSequences ?? []; } - - /// - /// Initializes a new instance of the class. - /// - /// The model ID to use for the request. - /// The messages to send to the model. - /// The maximum number of tokens to generate. - /// The system prompt to use for the request. - /// The metadata to include with the request. - /// The temperature to use for the request. - /// The top-K value to use for the request. - /// The top-P value to use for the request. - /// The tool choice mode to use for the request. - /// The tools to use for the request. - /// A value indicating whether the message should be streamed. - /// The prompt stop sequences. - /// Thrown when the model ID is invalid. - /// Thrown when the model or messages is null. - /// Thrown when the messages contain no messages. - /// Thrown when the max tokens is less than one. - /// Thrown when the temperature is less than zero or greater than one. - /// A new instance of the class. - protected BaseMessageRequest( - string model, - List messages, - int maxTokens = 1024, - string? system = null, - Dictionary? metadata = null, - decimal temperature = 0.0m, - int? topK = null, - decimal? topP = null, - ToolChoice? toolChoice = null, - List? tools = null, - bool stream = false, - List? stopSequences = null - ) : this( - model, - messages, - maxTokens, - system, - null, - metadata, - temperature, - topK, - topP, - toolChoice, - tools, - stream, - stopSequences - ) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The model ID to use for the request. - /// The messages to send to the model. - /// The maximum number of tokens to generate. - /// The system messages to send to the model to be used as the system prompt. - /// The metadata to include with the request. - /// The temperature to use for the request. - /// The top-K value to use for the request. - /// The top-P value to use for the request. - /// The tool choice mode to use for the request. - /// The tools to use for the request. - /// A value indicating whether the message should be streamed. - /// The prompt stop sequences. - /// Thrown when the model ID is invalid. - /// Thrown when the model or messages is null. - /// Thrown when the messages contain no messages. - /// Thrown when the max tokens is less than one. - /// Thrown when the temperature is less than zero or greater than one. - /// A new instance of the class. - protected BaseMessageRequest( - string model, - List messages, - int maxTokens = 1024, - List? systemMessages = null, - Dictionary? metadata = null, - decimal temperature = 0.0m, - int? topK = null, - decimal? topP = null, - ToolChoice? toolChoice = null, - List? tools = null, - bool stream = false, - List? stopSequences = null - ) : this( - model, - messages, - maxTokens, - null, - systemMessages, - metadata, - temperature, - topK, - topP, - toolChoice, - tools, - stream, - stopSequences - ) - { - } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageRequest.cs b/src/AnthropicClient/Models/MessageRequest.cs index fab7af1..f091dbf 100644 --- a/src/AnthropicClient/Models/MessageRequest.cs +++ b/src/AnthropicClient/Models/MessageRequest.cs @@ -16,7 +16,7 @@ public class MessageRequest : BaseMessageRequest /// The model ID to use for the request. /// The messages to send to the model. /// The maximum number of tokens to generate. - /// The system ID to use for the request. + /// The system prompt to use for the request. /// The metadata to include with the request. /// The temperature to use for the request. /// The top-K value to use for the request. @@ -24,6 +24,7 @@ public class MessageRequest : BaseMessageRequest /// The tool choice mode to use for the request. /// The tools to use for the request. /// The prompt stop sequences. + /// The system messages to include with the request. /// Thrown when the model ID is invalid. /// Thrown when the model or messages is null. /// Thrown when the messages contain no messages. @@ -41,7 +42,8 @@ public class MessageRequest : BaseMessageRequest decimal? topP = null, ToolChoice? toolChoice = null, List? tools = null, - List? stopSequences = null + List? stopSequences = null, + List? systemMessages = null ) : base( model, messages, @@ -54,7 +56,8 @@ public class MessageRequest : BaseMessageRequest toolChoice, tools, false, - stopSequences + stopSequences, + systemMessages ) { } diff --git a/src/AnthropicClient/Models/StreamMessageRequest.cs b/src/AnthropicClient/Models/StreamMessageRequest.cs index 9ef0e0b..402075e 100644 --- a/src/AnthropicClient/Models/StreamMessageRequest.cs +++ b/src/AnthropicClient/Models/StreamMessageRequest.cs @@ -16,7 +16,7 @@ public class StreamMessageRequest : BaseMessageRequest /// The model ID to use for the request. /// The messages to send to the model. /// The maximum number of tokens to generate. - /// The system ID to use for the request. + /// The system prompt to use for the request. /// The metadata to include with the request. /// The temperature to use for the request. /// The top-K value to use for the request. @@ -24,6 +24,7 @@ public class StreamMessageRequest : BaseMessageRequest /// The tool choice mode to use for the request. /// The tools to use for the request. /// The prompt stop sequences. + /// The system messages to include with the request. /// Thrown when the model ID is invalid. /// Thrown when the model or messages is null. /// Thrown when the messages contain no messages. @@ -41,7 +42,8 @@ public class StreamMessageRequest : BaseMessageRequest decimal? topP = null, ToolChoice? toolChoice = null, List? tools = null, - List? stopSequences = null + List? stopSequences = null, + List? systemMessages = null ) : base( model, messages, @@ -54,7 +56,8 @@ public class StreamMessageRequest : BaseMessageRequest toolChoice, tools, true, - stopSequences + stopSequences, + systemMessages ) { } From 35adac9fa0f3156c07e1fc4324b8fb2c1b50acd8 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:58:14 -0500 Subject: [PATCH 05/26] feat: add type for ephemeral cache control --- src/AnthropicClient/Models/EphemeralCacheControl.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/AnthropicClient/Models/EphemeralCacheControl.cs diff --git a/src/AnthropicClient/Models/EphemeralCacheControl.cs b/src/AnthropicClient/Models/EphemeralCacheControl.cs new file mode 100644 index 0000000..05185ce --- /dev/null +++ b/src/AnthropicClient/Models/EphemeralCacheControl.cs @@ -0,0 +1,13 @@ +namespace AnthropicClient.Models; + +/// +/// Represents the cache control to be used for content. +/// +public class EphemeralCacheControl : CacheControl +{ + /// + /// Initializes a new instance of the class. + /// + /// A new instance of the class. + public EphemeralCacheControl() : base(CacheControlType.Ephemeral) { } +} \ No newline at end of file From 987fbe974236379f788df4fca2f0326901b2c094 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:51:32 -0500 Subject: [PATCH 06/26] fix: reuse validation logic in constructors --- src/AnthropicClient/Models/ImageContent.cs | 12 ++++++++---- src/AnthropicClient/Models/TextContent.cs | 11 ++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/AnthropicClient/Models/ImageContent.cs b/src/AnthropicClient/Models/ImageContent.cs index 20c071c..b096fab 100644 --- a/src/AnthropicClient/Models/ImageContent.cs +++ b/src/AnthropicClient/Models/ImageContent.cs @@ -19,6 +19,12 @@ public class ImageContent : Content { } + private void Validate(string mediaType, string data) + { + ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType)); + ArgumentValidator.ThrowIfNull(data, nameof(data)); + } + /// /// Initializes a new instance of the class. /// @@ -28,8 +34,7 @@ public class ImageContent : Content /// A new instance of the class. public ImageContent(string mediaType, string data) : base(ContentType.Image) { - ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType)); - ArgumentValidator.ThrowIfNull(data, nameof(data)); + Validate(mediaType, data); Source = new(mediaType, data); } @@ -44,8 +49,7 @@ public class ImageContent : Content /// Thrown when the media type, data, or cache control is null. public ImageContent(string mediaType, string data, CacheControl cacheControl) : base(ContentType.Image, cacheControl) { - ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType)); - ArgumentValidator.ThrowIfNull(data, nameof(data)); + Validate(mediaType, data); Source = new(mediaType, data); } diff --git a/src/AnthropicClient/Models/TextContent.cs b/src/AnthropicClient/Models/TextContent.cs index 01b14ee..b9827bc 100644 --- a/src/AnthropicClient/Models/TextContent.cs +++ b/src/AnthropicClient/Models/TextContent.cs @@ -19,6 +19,11 @@ public class TextContent : Content { } + private void Validate(string text) + { + ArgumentValidator.ThrowIfNull(text, nameof(text)); + } + /// /// Initializes a new instance of the class. /// @@ -27,7 +32,7 @@ public class TextContent : Content /// A new instance of the class. public TextContent(string text) : base(ContentType.Text) { - ArgumentValidator.ThrowIfNull(text, nameof(text)); + Validate(text); Text = text; } @@ -41,8 +46,8 @@ public class TextContent : Content /// Thrown when the text or cache control is null. public TextContent(string text, CacheControl cacheControl) : base(ContentType.Text, cacheControl) { - ArgumentValidator.ThrowIfNull(text, nameof(text)); - + Validate(text); + Text = text; } } \ No newline at end of file From 6431e2a113b9a87a53c4a1ddd2c1ef49f5e3e50d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:51:45 -0500 Subject: [PATCH 07/26] fix: make cache control class abstract --- src/AnthropicClient/Models/CacheControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AnthropicClient/Models/CacheControl.cs b/src/AnthropicClient/Models/CacheControl.cs index d29dafb..b39e487 100644 --- a/src/AnthropicClient/Models/CacheControl.cs +++ b/src/AnthropicClient/Models/CacheControl.cs @@ -5,7 +5,7 @@ namespace AnthropicClient.Models; /// /// Represents the cache control to be used for content. /// -public class CacheControl +public abstract class CacheControl { /// /// Gets the type of the cache control. @@ -18,7 +18,7 @@ public class CacheControl /// The type of the cache control. /// A new instance of the class. /// Thrown when the type is null or whitespace. - public CacheControl(string type) + protected CacheControl(string type) { ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type)); From 364c91080c463fcb2249dfee37745c8ff20c7586 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:51:56 -0500 Subject: [PATCH 08/26] tests: add test for cache control type static class --- .../Unit/Models/CacheControlTypeTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/AnthropicClient.Tests/Unit/Models/CacheControlTypeTests.cs diff --git a/tests/AnthropicClient.Tests/Unit/Models/CacheControlTypeTests.cs b/tests/AnthropicClient.Tests/Unit/Models/CacheControlTypeTests.cs new file mode 100644 index 0000000..4eecb88 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/CacheControlTypeTests.cs @@ -0,0 +1,10 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class CacheControlTypeTests +{ + [Fact] + public void Ephemeral_WhenCalled_ItShouldReturnExpectedValue() + { + CacheControlType.Ephemeral.Should().Be("ephemeral"); + } +} \ No newline at end of file From f2f150791ec5ba4bced53094bafbc95e8f9ae087 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:53:03 -0500 Subject: [PATCH 09/26] tests: add tests for ephemeral cache control model --- .../Unit/Models/EphemeralCacheControlTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/AnthropicClient.Tests/Unit/Models/EphemeralCacheControlTests.cs diff --git a/tests/AnthropicClient.Tests/Unit/Models/EphemeralCacheControlTests.cs b/tests/AnthropicClient.Tests/Unit/Models/EphemeralCacheControlTests.cs new file mode 100644 index 0000000..8e2b486 --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/EphemeralCacheControlTests.cs @@ -0,0 +1,10 @@ +namespace AnthropicClient.Tests.Unit.Models; + +public class EphemeralCacheControlTests +{ + [Fact] + public void Constructor_WhenCalled_ItShouldInitializeType() + { + new EphemeralCacheControl().Type.Should().Be(CacheControlType.Ephemeral); + } +} \ No newline at end of file From 025379423527677d9fab77ca92a5ca4004ea1571 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:53:20 -0500 Subject: [PATCH 10/26] tests: add tests for overloaded constructor --- .../Unit/Models/ImageContentTests.cs | 55 +++++++++++++++++++ .../Unit/Models/TextContentTests.cs | 37 +++++++++++++ 2 files changed, 92 insertions(+) diff --git a/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs index fbc6ece..bcc4e50 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ImageContentTests.cs @@ -11,6 +11,16 @@ public class ImageContentTests : SerializationTest ""type"": ""image"" }"; + private readonly string _testJsonWithCacheControl = @"{ + ""source"": { + ""media_type"": ""image/png"", + ""data"": ""data"", + ""type"": ""base64"" + }, + ""cache_control"": { ""type"": ""ephemeral"" }, + ""type"": ""image"" + }"; + [Fact] public void Constructor_WhenCalled_ItShouldInitializeSource() { @@ -53,6 +63,41 @@ public class ImageContentTests : SerializationTest action.Should().Throw(); } + [Fact] + public void Constructor_WhenCalledWithCacheControl_ItShouldInitializeSourceAndCacheControl() + { + var expectedMediaType = "image/png"; + var expectedData = "data"; + var cacheControl = new EphemeralCacheControl(); + + var result = new ImageContent(expectedMediaType, expectedData, cacheControl); + + result.Source.Should().BeEquivalentTo(new ImageSource(expectedMediaType, expectedData)); + result.CacheControl.Should().BeSameAs(cacheControl); + } + + [Fact] + public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException() + { + var expectedData = "data"; + var cacheControl = new EphemeralCacheControl(); + + var action = () => new ImageContent(null!, expectedData, cacheControl); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledWithCacheControlAndDataIsNull_ItShouldThrowArgumentNullException() + { + var expectedMediaType = "image/png"; + var cacheControl = new EphemeralCacheControl(); + + var action = () => new ImageContent(expectedMediaType, null!, cacheControl); + + action.Should().Throw(); + } + [Fact] public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() { @@ -63,6 +108,16 @@ public class ImageContentTests : SerializationTest JsonAssert.Equal(_testJson, actual); } + [Fact] + public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldHaveExpectedShape() + { + var content = new ImageContent("image/png", "data", new EphemeralCacheControl()); + + var actual = Serialize(content); + + JsonAssert.Equal(_testJsonWithCacheControl, actual); + } + [Fact] public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() { diff --git a/tests/AnthropicClient.Tests/Unit/Models/TextContentTests.cs b/tests/AnthropicClient.Tests/Unit/Models/TextContentTests.cs index 4c54c0a..03301d2 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/TextContentTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/TextContentTests.cs @@ -3,6 +3,11 @@ namespace AnthropicClient.Tests.Unit.Models; public class TextContentTests : SerializationTest { private readonly string _testJson = @"{ ""text"": ""text"", ""type"": ""text"" }"; + private readonly string _testJsonWithCacheControl = @"{ + ""text"": ""text"", + ""cache_control"": { ""type"": ""ephemeral"" }, + ""type"": ""text"" + }"; [Fact] public void Constructor_WhenCalled_ItShouldInitializeText() @@ -14,6 +19,18 @@ public class TextContentTests : SerializationTest result.Text.Should().Be(expectedText); } + [Fact] + public void Constructor_WhenCalledWithCacheControl_ItShouldInitializeProperties() + { + var expectedText = "text"; + var cacheControl = new EphemeralCacheControl(); + + var result = new TextContent(expectedText, cacheControl); + + result.Text.Should().Be(expectedText); + result.CacheControl.Should().BeSameAs(cacheControl); + } + [Fact] public void Constructor_WhenCalledAndTextIsNull_ItShouldThrowArgumentNullException() { @@ -22,6 +39,16 @@ public class TextContentTests : SerializationTest action.Should().Throw(); } + [Fact] + public void Constructor_WhenCalledWithCacheControlAndTextIsNull_ItShouldThrowArgumentNullException() + { + var cacheControl = new EphemeralCacheControl(); + + var action = () => new TextContent(null!, cacheControl); + + action.Should().Throw(); + } + [Fact] public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() { @@ -32,6 +59,16 @@ public class TextContentTests : SerializationTest JsonAssert.Equal(_testJson, actual); } + [Fact] + public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldHaveExpectedShape() + { + var content = new TextContent("text", new EphemeralCacheControl()); + + var actual = Serialize(content); + + JsonAssert.Equal(_testJsonWithCacheControl, actual); + } + [Fact] public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() { From d458c9ed23a4f575304d0c16db591c1081d6d118 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 17:56:07 -0500 Subject: [PATCH 11/26] fix: make cache control setter public --- src/AnthropicClient/Models/Content.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnthropicClient/Models/Content.cs b/src/AnthropicClient/Models/Content.cs index 946b8c5..a8aa0dd 100644 --- a/src/AnthropicClient/Models/Content.cs +++ b/src/AnthropicClient/Models/Content.cs @@ -16,7 +16,7 @@ public abstract class Content /// Gets the cache control to be used for the content. /// [JsonPropertyName("cache_control")] - public CacheControl? CacheControl { get; init; } + public CacheControl? CacheControl { get; set; } [JsonConstructor] internal Content() From a3c7e9073465a0db3a69927f0b8361e9b670266d Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sat, 17 Aug 2024 18:08:22 -0500 Subject: [PATCH 12/26] feat: add constructor to allow setting cache control on tool result content --- .../Models/ToolResultContent.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/AnthropicClient/Models/ToolResultContent.cs b/src/AnthropicClient/Models/ToolResultContent.cs index 1ada98c..634ac87 100644 --- a/src/AnthropicClient/Models/ToolResultContent.cs +++ b/src/AnthropicClient/Models/ToolResultContent.cs @@ -23,6 +23,12 @@ public class ToolResultContent : Content [JsonConstructor] internal ToolResultContent() : base(ContentType.ToolResult) { } + private void Validate(string toolUseId, string content) + { + ArgumentValidator.ThrowIfNull(toolUseId, nameof(toolUseId)); + ArgumentValidator.ThrowIfNull(content, nameof(content)); + } + /// /// Initializes a new instance of the class. /// @@ -32,10 +38,26 @@ public class ToolResultContent : Content /// A new instance of the class. public ToolResultContent(string toolUseId, string content) : base(ContentType.ToolResult) { - ArgumentValidator.ThrowIfNull(toolUseId, nameof(toolUseId)); - ArgumentValidator.ThrowIfNull(content, nameof(content)); + Validate(toolUseId, content); ToolUseId = toolUseId; Content = content; } + + /// + /// Initializes a new instance of the class. + /// + /// The tool use ID of the content. + /// The content of the tool result. + /// The cache control to be used for the content. + /// Thrown when the tool use ID or content is null. + /// A new instance of the class. + public ToolResultContent(string toolUseId, string content, CacheControl cacheControl) : base(ContentType.ToolResult) + { + Validate(toolUseId, content); + + ToolUseId = toolUseId; + Content = content; + CacheControl = cacheControl; + } } \ No newline at end of file From 0c160e998a628af0f21883e592ddc8fb902f170f Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 11:17:29 -0500 Subject: [PATCH 13/26] tests: add tests for constructor using cache control --- .../Unit/Models/ToolResultContentTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolResultContentTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolResultContentTests.cs index 0761f00..c15b209 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolResultContentTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolResultContentTests.cs @@ -35,6 +35,43 @@ public class ToolResultContentTests : SerializationTest action.Should().Throw(); } + [Fact] + public void Constructor_WhenCalledAndGivenCacheControl_ItShouldInitializeProperties() + { + var toolUseId = Guid.NewGuid().ToString(); + var content = "content"; + var cacheControl = new EphemeralCacheControl(); + + var actual = new ToolResultContent(toolUseId, content, cacheControl); + + actual.ToolUseId.Should().Be(toolUseId); + actual.Content.Should().Be(content); + actual.CacheControl.Should().BeSameAs(cacheControl); + actual.Type.Should().Be("tool_result"); + } + + [Fact] + public void Constructor_WhenCalledAndGivenCacheControlAndToolUseIdIsNull_ItShouldThrowArgumentNullException() + { + var content = "content"; + var cacheControl = new EphemeralCacheControl(); + + var action = () => new ToolResultContent(null!, content, cacheControl); + + action.Should().Throw(); + } + + [Fact] + public void Constructor_WhenCalledAndGivenCacheControlAndContentIsNull_ItShouldThrowArgumentNullException() + { + var toolUseId = Guid.NewGuid().ToString(); + var cacheControl = new EphemeralCacheControl(); + + var action = () => new ToolResultContent(toolUseId, null!, cacheControl); + + action.Should().Throw(); + } + [Fact] public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString() { @@ -58,6 +95,34 @@ public class ToolResultContentTests : SerializationTest JsonAssert.Equal(expectedJson, actual); } + [Fact] + public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldReturnJsonString() + { + var toolUseId = Guid.NewGuid().ToString(); + var content = "content"; + var cacheControl = new EphemeralCacheControl(); + + var expectedJson = @$"{{ + ""tool_use_id"": ""{toolUseId}"", + ""content"": ""{content}"", + ""type"": ""tool_result"", + ""cache_control"": {{ + ""type"": ""ephemeral"" + }} + }}"; + + var toolResultContent = new ToolResultContent + { + ToolUseId = toolUseId, + Content = content, + CacheControl = cacheControl + }; + + var actual = Serialize(toolResultContent); + + JsonAssert.Equal(expectedJson, actual); + } + [Fact] public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolResultContent() { From e47095e03dc49d5a3954f37cc928b3eaced04779 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 11:20:19 -0500 Subject: [PATCH 14/26] tests: add test to make sure cache control can be set on tool use content objects --- .../Unit/Models/ToolUseContentTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolUseContentTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolUseContentTests.cs index 56e55a4..28f3855 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolUseContentTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolUseContentTests.cs @@ -22,6 +22,26 @@ public class ToolUseContentTests : SerializationTest actual.Type.Should().Be("tool_use"); } + [Fact] + public void CacheControl_WhenCalledToSetCacheControl_ItShouldSetCacheControl() + { + var id = Guid.NewGuid().ToString(); + var name = "name"; + var input = new Dictionary { { "name", "input" } }; + var cacheControl = new EphemeralCacheControl(); + + var toolUseContent = new ToolUseContent() + { + Id = id, + Name = name, + Input = input + }; + + toolUseContent.CacheControl = cacheControl; + + toolUseContent.CacheControl.Should().BeSameAs(cacheControl); + } + [Fact] public void JsonSerialization_WhenSerialized_ItShouldReturnJsonString() { @@ -48,6 +68,35 @@ public class ToolUseContentTests : SerializationTest JsonAssert.Equal(expectedJson, actual); } + [Fact] + public void JsonSerialization_WhenSerializedWithCacheControl_ItShouldReturnJsonString() + { + var id = Guid.NewGuid().ToString(); + var name = "name"; + var input = new Dictionary { { "name", "input" } }; + var cacheControl = new EphemeralCacheControl(); + + var expectedJson = @$"{{ + ""id"": ""{id}"", + ""name"": ""{name}"", + ""input"": {{ ""name"": ""input"" }}, + ""cache_control"": {{ ""type"": ""ephemeral"" }}, + ""type"": ""tool_use"" + }}"; + + var toolUseContent = new ToolUseContent() + { + Id = id, + Name = name, + Input = input, + CacheControl = cacheControl + }; + + var actual = Serialize(toolUseContent); + + JsonAssert.Equal(expectedJson, actual); + } + [Fact] public void JsonDeserialization_WhenDeserialized_ItShouldReturnToolUseContent() { From f6460e99ee5195342b63b71e4e1a720eaaf4432e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 11:37:19 -0500 Subject: [PATCH 15/26] tests: add test for serializing system property with correct expected value based on whether given system messages or just a system message. --- .../Unit/Models/MessageRequestTests.cs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs index bcd3532..6f96c54 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs @@ -322,6 +322,137 @@ public class MessageRequestTests : SerializationTest JsonAssert.Equal(_testJson, actual); } + [Fact] + public void JsonSerialization_WhenSerializedAndSystemMessagesAndSystemAreNull_ItShouldNotHaveSystemProperty() + { + var messageRequest = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [ + new() + { + Role = MessageRole.User, + Content = [new TextContent("Hello!")] + } + ] + ); + + var expected = @"{ + ""model"": ""claude-3-haiku-20240307"", + ""messages"": [ + { + ""role"": ""user"", + ""content"": [ + { + ""text"": ""Hello!"", + ""type"": ""text"" + } + ] + } + ], + ""max_tokens"": 1024, + ""stop_sequences"": [], + ""temperature"": 0.0, + ""stream"": false + }"; + + var actual = Serialize(messageRequest); + + JsonAssert.Equal(expected, actual); + } + + [Fact] + public void JsonSerialization_WhenSerializedAndSystemMessagesAreProvided_ItShouldUseSystemMessagesForHaveSystemProperty() + { + var messageRequest = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [ + new() + { + Role = MessageRole.User, + Content = [new TextContent("Hello!")] + } + ], + systemMessages: [ + new TextContent("You are a helpful assistant.") + ], + system: "test-system" + ); + + var expected = @"{ + ""model"": ""claude-3-haiku-20240307"", + ""system"": [ + { + ""text"": ""You are a helpful assistant."", + ""type"": ""text"" + } + ], + ""messages"": [ + { + ""role"": ""user"", + ""content"": [ + { + ""text"": ""Hello!"", + ""type"": ""text"" + } + ] + } + ], + ""max_tokens"": 1024, + ""stop_sequences"": [], + ""temperature"": 0.0, + ""stream"": false + }"; + + var actual = Serialize(messageRequest); + + JsonAssert.Equal(expected, actual); + } + + [Fact] + public void JsonSerialization_WhenSerializedAndSystemMessageIsProvided_ItShouldUseSystemMessageForHaveSystemProperty() + { + var messageRequest = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [ + new() + { + Role = MessageRole.User, + Content = [new TextContent("Hello!")] + } + ], + system: "test-system" + ); + + var expected = @"{ + ""model"": ""claude-3-haiku-20240307"", + ""system"": [ + { + ""text"": ""test-system"", + ""type"": ""text"" + } + ], + ""messages"": [ + { + ""role"": ""user"", + ""content"": [ + { + ""text"": ""Hello!"", + ""type"": ""text"" + } + ] + } + ], + ""max_tokens"": 1024, + ""stop_sequences"": [], + ""temperature"": 0.0, + ""stream"": false + }"; + + var actual = Serialize(messageRequest); + + JsonAssert.Equal(expected, actual); + } + [Fact] public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedShape() { From cc1d628feb482655c0b2b1b4675f98fc47f5f584 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:52:21 -0500 Subject: [PATCH 16/26] fix: give cache control proper json property name --- src/AnthropicClient/Models/Tool.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AnthropicClient/Models/Tool.cs b/src/AnthropicClient/Models/Tool.cs index e861568..45e61e1 100644 --- a/src/AnthropicClient/Models/Tool.cs +++ b/src/AnthropicClient/Models/Tool.cs @@ -58,6 +58,7 @@ public class Tool /// /// Gets or sets the cache control to be used for the tool. /// + [JsonPropertyName("cache_control")] public CacheControl? CacheControl { get; set; } /// From 2e1c1118c36878cedfde8fee7a645eadfa1ed418 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:52:44 -0500 Subject: [PATCH 17/26] tests: add text greater than 2048 tokens for testing caching --- tests/AnthropicClient.Tests/Files/story.txt | 69 +++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/AnthropicClient.Tests/Files/story.txt diff --git a/tests/AnthropicClient.Tests/Files/story.txt b/tests/AnthropicClient.Tests/Files/story.txt new file mode 100644 index 0000000..df98a64 --- /dev/null +++ b/tests/AnthropicClient.Tests/Files/story.txt @@ -0,0 +1,69 @@ +The Forgotten Lighthouse + +Sarah had always been drawn to the sea. As a child, she would spend hours on the beach, collecting shells and watching the waves crash against the shore. Now, at 28, she found herself living in a small coastal town, working as a marine biologist at the local research center. It was her dream job, allowing her to study the ocean and its inhabitants up close. + +One stormy evening, as Sarah was walking along the beach after work, she noticed something peculiar in the distance. Through the mist and rain, she could make out the faint outline of a lighthouse she had never seen before. Intrigued, she decided to investigate. + +As she approached the structure, Sarah realized why she had never noticed it before. The lighthouse was in a state of disrepair, its once-white paint now peeling and faded. Vines and moss clung to its sides, as if nature was slowly reclaiming it. The beacon at the top was dark, and Sarah wondered how long it had been since it had last guided ships to safety. + +Despite the dilapidated appearance, there was something enchanting about the old lighthouse. Sarah felt drawn to it, as if it held secrets waiting to be discovered. She circled the base, looking for an entrance, and found a rusty door that creaked open with a gentle push. + +Inside, the air was musty and thick with dust. Sarah pulled out her phone and turned on the flashlight, illuminating the circular room. Old furniture was scattered about, covered in sheets that had long since turned gray. In the center stood a spiral staircase, leading up to the top of the lighthouse. + +Sarah hesitated for a moment, wondering if it was safe to climb the stairs in such an old building. But her curiosity got the better of her, and she began to ascend, each step groaning under her weight. + +As she climbed, Sarah noticed old photographs hanging on the walls. They showed a family – a lighthouse keeper, his wife, and their young daughter – smiling in front of the once-pristine lighthouse. The images were faded and yellowed with age, but Sarah could still make out the happiness in their eyes. + +When she reached the top, Sarah gasped. The view was breathtaking, even through the dirty windows. She could see for miles in every direction, the stormy sea stretching out to the horizon. The room was filled with old equipment – logbooks, maps, and a massive lens that once projected the lighthouse's beam across the water. + +As Sarah explored the room, she noticed something odd. Despite the layer of dust covering everything, there was a small area on the desk that seemed clean, as if someone had recently been there. Next to it lay an old leather-bound journal. + +Curious, Sarah picked up the journal and opened it. The pages were filled with neat handwriting, detailing the daily life of the lighthouse keeper. As she flipped through the pages, she realized that the entries spanned decades, far longer than one person's lifetime. + +The last entry caught her eye. It was dated just a week ago: + +"I've been here for so long, watching over the sea and guiding ships to safety. But times have changed, and my lighthouse is no longer needed. I fear I may soon fade away, just like the light I once tended. If anyone finds this journal, please remember us – the keepers of the light." + +Sarah's hands trembled as she read the words. She looked around the room, half-expecting to see a ghost, but she was alone. As she turned back to the journal, a photograph slipped out from between the pages. It showed the same family she had seen in the pictures on the stairway, but this one was different. The image was crisp and clear, as if it had been taken recently, yet the people in it were dressed in old-fashioned clothes. + +A chill ran down Sarah's spine. She quickly put the journal back on the desk and hurried down the stairs, her heart pounding. As she reached the bottom and stepped outside, she turned to look at the lighthouse one last time. + +To her amazement, the lighthouse now appeared pristine and newly painted. The beacon at the top was shining brightly, cutting through the stormy night. Sarah rubbed her eyes, certain she must be seeing things, but when she looked again, the lighthouse was back to its dilapidated state. + +Over the next few weeks, Sarah couldn't stop thinking about her experience at the lighthouse. She searched through town records and old newspapers, trying to find any information about the mysterious structure and its keepers. To her surprise, she found nothing. It was as if the lighthouse had never existed. + +Determined to uncover the truth, Sarah returned to the lighthouse several times. Each visit left her with more questions than answers. Sometimes she would find fresh flowers on the desk upstairs, other times she would hear faint whispers or the sound of footsteps when she knew she was alone. + +As months passed, Sarah became known in town as the woman obsessed with the old lighthouse. Some thought she was crazy, while others were intrigued by her tales. A few of the older residents even claimed to have seen the lighthouse shining on stormy nights, guiding ships to safety long after it had been abandoned. + +Sarah's obsession began to affect her work and personal life. She spent less time at the research center and more time investigating the lighthouse's history. Her colleagues worried about her, but Sarah couldn't let go of the mystery. + +One night, exactly a year after her first visit to the lighthouse, Sarah decided to spend the night there. She packed a sleeping bag, some food, and her camera, determined to capture any supernatural occurrences. + +As she settled in for the night, the wind outside picked up, and rain began to lash against the windows. Sarah felt a mix of excitement and fear as she lay in her sleeping bag, watching the shadows dance on the walls. + +Just as she was about to drift off to sleep, Sarah heard a sound that made her blood run cold. It was the clear, unmistakable sound of footsteps climbing the spiral staircase. She held her breath, her heart pounding in her chest, as the steps grew louder and closer. + +The door to the room creaked open, and Sarah squeezed her eyes shut, too terrified to look. She felt a presence in the room, moving around her. Then, to her surprise, she heard a kind, elderly voice. + +"Don't be afraid, my dear. We've been waiting for someone like you." + +Sarah opened her eyes to find the room filled with a soft, warm light. Standing before her were the lighthouse keeper and his family from the photographs, smiling gently at her. + +The keeper extended his hand to Sarah. "We've been looking for someone to take over our duties. Someone who loves the sea as much as we do. Will you join us and become the new keeper of the light?" + +Sarah looked at the family, then out at the stormy sea beyond the windows. She thought about her life in town, her job at the research center, and the mystery that had consumed her for the past year. In that moment, she realized that she had never felt more at home than she did in this old lighthouse. + +With a smile, Sarah took the keeper's hand and stood up. As she did, she felt a strange sensation, as if she were becoming part of the lighthouse itself. The years of decay melted away, and the beacon blazed to life, sending its light out across the turbulent waters. + +From that night on, sailors would tell stories of the mysterious lighthouse that would appear on the darkest, stormiest nights, guiding them safely to shore. And if they looked closely, they might catch a glimpse of a young woman in the tower, keeping watch over the sea. + +The town eventually forgot about Sarah, the marine biologist who had become obsessed with an old lighthouse. But on quiet nights, when the mist rolls in from the sea, some say they can still hear her laughter on the wind, eternally at peace in her new home by the sea. + +Years passed, and the legend of the mysterious lighthouse grew. Sailors from all over the world shared tales of its miraculous appearances during treacherous storms. Some claimed it had saved them from certain doom, guiding them away from hidden reefs and dangerous shoals. Others spoke of catching glimpses of ghostly figures in the tower, tending to the light with unwavering dedication. + +The small coastal town, once skeptical of Sarah's obsession, began to embrace the legend. Local artists painted scenes of the lighthouse, its beam cutting through stormy skies. Gift shops sold miniature replicas and postcards featuring artistic renderings of the structure. The town even started an annual festival called "The Keeper's Light," celebrating the mysterious lighthouse and its guardians. + +As decades went by, the world changed. Modern navigation systems and GPS technology made traditional lighthouses obsolete. Many were decommissioned or turned into museums. But the forgotten lighthouse continued to appear when it was needed most, defying explanation and technology alike. + +One day, a young girl named Emily, not unlike Sarah had been in her youth, stumbled upon an old journal in her grandmother's attic. As she read through the faded pages, she discovered the story of a marine biologist who had disappeared decades ago, leaving behind tales of a magical lighthouse. Intrigued, Emily felt a familiar pull towards the sea and the mysteries it held. And so, the cycle began anew, as another curious soul prepared to uncover the secrets of the forgotten lighthouse, ensuring that its light – and the memory of its keepers – would never truly fade away. \ No newline at end of file From e04599bad9c930d8101ace78ee9c1d234f93e52e Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:53:08 -0500 Subject: [PATCH 18/26] chore: put request json in intermediate variable to make debugging better --- src/AnthropicClient/AnthropicApiClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 2c57cdd..7ec3b10 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -275,8 +275,9 @@ public class AnthropicApiClient : IAnthropicApiClient } private async Task SendRequestAsync(BaseMessageRequest request) - { - var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType); + { + var requestJson = Serialize(request); + var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType); return await _httpClient.PostAsync(MessagesEndpoint, requestContent); } From eb0befe62a1585c7fe26e0e1ff616e27b6dd52ff Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:53:42 -0500 Subject: [PATCH 19/26] tests: add method for creating client with customized http client --- tests/AnthropicClient.Tests/EndToEnd/EndToEndTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/AnthropicClient.Tests/EndToEnd/EndToEndTest.cs b/tests/AnthropicClient.Tests/EndToEnd/EndToEndTest.cs index 6e9b19b..7086d37 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/EndToEndTest.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/EndToEndTest.cs @@ -3,4 +3,5 @@ namespace AnthropicClient.Tests.EndToEnd; public class EndToEndTest(ConfigurationFixture configFixture) : IClassFixture { protected readonly AnthropicApiClient _client = new(configFixture.AnthropicApiKey, new()); + protected AnthropicApiClient CreateClient(HttpClient httpClient) => new(configFixture.AnthropicApiKey, httpClient); } \ No newline at end of file From 4524d7be8880a3014656ccb0b6b91ec9b76c0c89 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:54:13 -0500 Subject: [PATCH 20/26] tests: added end to end tests for cache control when caching system messages, user messages, or tools --- .../EndToEnd/AnthropicApiClientTests.cs | 134 +++++++++++++++++- 1 file changed, 130 insertions(+), 4 deletions(-) diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index 01cb07a..5b7a3cd 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -1,10 +1,10 @@ -using Xunit.Abstractions; -using Xunit.Sdk; - namespace AnthropicClient.Tests.EndToEnd; public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) { + private string GetTestFilePath(string fileName) => + Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName); + [Fact] public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse() { @@ -63,7 +63,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf [Fact] public async Task CreateMessageAsync_WhenImageIsSent_ItShouldReturnResponse() { - var imagePath = Path.Combine(Directory.GetCurrentDirectory(), "Files", "elephant.jpg"); + var imagePath = GetTestFilePath("elephant.jpg"); var mediaType = "image/jpeg"; var bytes = await File.ReadAllBytesAsync(imagePath); var base64Data = Convert.ToBase64String(bytes); @@ -96,4 +96,130 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf text.Should().Contain("elephant"); } + + [Fact] + public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache() + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31"); + + var client = CreateClient(httpClient); + + var storyPath = GetTestFilePath("story.txt"); + var storyText = await File.ReadAllTextAsync(storyPath); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + systemMessages: [ + new("You are a helpful assistant who can answer questions about the following text:"), + new(storyText, new EphemeralCacheControl()) + ], + messages: [ + new(MessageRole.User, [ + new TextContent("Give me a one sentence summary of this story.") + ]), + ] + ); + + var resultOne = await client.CreateMessageAsync(request); + + resultOne.IsSuccess.Should().BeTrue(); + resultOne.Value.Should().BeOfType(); + resultOne.Value.Content.Should().NotBeNullOrEmpty(); + resultOne.Value.Usage.Should().Match(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0); + + request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); + request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")])); + + var resultTwo = await client.CreateMessageAsync(request); + + resultTwo.IsSuccess.Should().BeTrue(); + resultTwo.Value.Should().BeOfType(); + resultTwo.Value.Content.Should().NotBeNullOrEmpty(); + resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0); + } + + [Fact] + public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache() + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31"); + + var client = CreateClient(httpClient); + + var storyPath = GetTestFilePath("story.txt"); + var storyText = await File.ReadAllTextAsync(storyPath); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [ + new(MessageRole.User, [ + new TextContent("Give me a one sentence summary of this story."), + new TextContent(storyText, new EphemeralCacheControl()) + ]), + ] + ); + + var resultOne = await client.CreateMessageAsync(request); + + resultOne.IsSuccess.Should().BeTrue(); + resultOne.Value.Should().BeOfType(); + resultOne.Value.Content.Should().NotBeNullOrEmpty(); + resultOne.Value.Usage.Should().Match(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0); + + request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); + request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")])); + + var resultTwo = await client.CreateMessageAsync(request); + + resultTwo.IsSuccess.Should().BeTrue(); + resultTwo.Value.Should().BeOfType(); + resultTwo.Value.Content.Should().NotBeNullOrEmpty(); + resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0); + } + + [Fact] + public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache() + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31"); + + var client = CreateClient(httpClient); + + var func = (string ticker) => ticker; + + var tools = Enumerable + .Range(0, 50) + .Select(i => Tool.CreateFromFunction($"tool-{i}", $"Tool {i}", func)) + .ToList(); + + tools.Last().CacheControl = new EphemeralCacheControl(); + + var request = new MessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [ + new(MessageRole.User, [ + new TextContent("Hi could you tell me your name?"), + ]), + ], + tools: tools + ); + + var resultOne = await client.CreateMessageAsync(request); + + resultOne.IsSuccess.Should().BeTrue(); + resultOne.Value.Should().BeOfType(); + resultOne.Value.Content.Should().NotBeNullOrEmpty(); + resultOne.Value.Usage.Should().Match(u => u.CacheCreationInputTokens > 0 || u.CacheReadInputTokens > 0); + + request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content)); + request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")])); + + var resultTwo = await client.CreateMessageAsync(request); + + resultTwo.IsSuccess.Should().BeTrue(); + resultTwo.Value.Should().BeOfType(); + resultTwo.Value.Content.Should().NotBeNullOrEmpty(); + resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0); + } } \ No newline at end of file From 07457c1808fc7b07bc28a5904a7dd6cfbc7053df Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:56:49 -0500 Subject: [PATCH 21/26] tests: remove unused using statement --- tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs index a95bdc5..d11e5dd 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Text.Json.Nodes; namespace AnthropicClient.Tests.Unit.Models; From 539d735e6b244715f1e30c67175bf8f2eb6f5729 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:19:43 -0500 Subject: [PATCH 22/26] fix: make sure to pass cache control to tool constructor --- src/AnthropicClient/Models/Tool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnthropicClient/Models/Tool.cs b/src/AnthropicClient/Models/Tool.cs index 45e61e1..66243fc 100644 --- a/src/AnthropicClient/Models/Tool.cs +++ b/src/AnthropicClient/Models/Tool.cs @@ -182,7 +182,7 @@ public class Tool throw new ArgumentException($"Method '{methodName}' not found in type '{instance.GetType().FullName}'.", nameof(methodName)); } - return new Tool(name, description, new AnthropicFunction(method, instance), null); + return new Tool(name, description, new AnthropicFunction(method, instance), cacheControl); } /// From 42d6ee4d3443f7d8a3962a07fd5f7fc6b919e5ff Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:19:55 -0500 Subject: [PATCH 23/26] tests: add tests for creating tools with cache control set --- .../Unit/Models/ToolTests.cs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs index d11e5dd..ba026b6 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/ToolTests.cs @@ -70,6 +70,17 @@ public class ToolTests : SerializationTest tool.Name.Should().HaveLength(64); } + [Fact] + public void Constructor_WhenCalledAndGivenCacheControl_ItShouldInitializeCacheControl() + { + var method = () => true; + var function = new AnthropicFunction(method.Method); + var cacheControl = new EphemeralCacheControl(); + var tool = new Tool("name", "description", function, cacheControl); + + tool.CacheControl.Should().BeSameAs(cacheControl); + } + [Theory] [InlineData(typeof(TestClass), null)] [InlineData(typeof(TestClass), "")] @@ -119,6 +130,29 @@ public class ToolTests : SerializationTest ); } + [Fact] + public void CreateFromStaticMethod_WhenCalledWithCacheControl_ItShouldReturnTool() + { + var cacheControl = new EphemeralCacheControl(); + var tool = Tool.CreateFromStaticMethod("test name", "description", typeof(TestClass), nameof(TestClass.TestStaticMethod), cacheControl); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + }; + + tool.Name.Should().Be("test_name"); + tool.DisplayName.Should().Be("test name"); + tool.Description.Should().Be("description"); + tool.Function.Method.Name.Should().Be(nameof(TestClass.TestStaticMethod)); + tool.Function.Instance.Should().BeNull(); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + tool.CacheControl.Should().BeSameAs(cacheControl); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -177,6 +211,30 @@ public class ToolTests : SerializationTest ); } + [Fact] + public void CreateFromInstanceMethod_WhenCalledWithCacheControl_ItShouldReturnTool() + { + var instance = new TestClass(); + var cacheControl = new EphemeralCacheControl(); + var tool = Tool.CreateFromInstanceMethod("test name", "description", instance, nameof(instance.TestInstanceMethod), cacheControl); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + }; + + tool.Name.Should().Be("test_name"); + tool.DisplayName.Should().Be("test name"); + tool.Description.Should().Be("description"); + tool.Function.Method.Name.Should().Be(nameof(TestClass.TestInstanceMethod)); + tool.Function.Instance.Should().Be(instance); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + tool.CacheControl.Should().BeSameAs(cacheControl); + } + [Fact] public void CreateFromParameterlessFunction_WhenGivenNullFunction_ItShouldThrowArgumentNullException() { @@ -206,6 +264,29 @@ public class ToolTests : SerializationTest ); } + [Fact] + public void CreateFromParameterlessFunction_WhenCalledWithCacheControl_ItShouldReturnTool() + { + var func = () => true; + var cacheControl = new EphemeralCacheControl(); + var tool = Tool.CreateFromFunction("test name", "description", func, cacheControl); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + }; + + tool.Name.Should().Be("test_name"); + tool.DisplayName.Should().Be("test name"); + tool.Description.Should().Be("description"); + tool.Function.Method.Should().BeSameAs(func.Method); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + tool.CacheControl.Should().BeSameAs(cacheControl); + } + [Fact] public void CreateFromFunctionWithParameter_WhenGivenNullFunction_ItShouldThrowArgumentNullException() { @@ -246,6 +327,40 @@ public class ToolTests : SerializationTest ); } + [Fact] + public void CreateFromFunctionWithParameter_WhenCalledWithCacheControl_ItShouldReturnTool() + { + var func = (string s) => true; + var cacheControl = new EphemeralCacheControl(); + var tool = Tool.CreateFromFunction("test name", "description", func, cacheControl); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + ["properties"] = new JsonObject() + { + ["s"] = new JsonObject() + { + ["type"] = "string", + }, + }, + ["required"] = new JsonArray() + { + "s", + }, + }; + + tool.Name.Should().Be("test_name"); + tool.DisplayName.Should().Be("test name"); + tool.Description.Should().Be("description"); + tool.Function.Method.Should().BeSameAs(func.Method); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + tool.CacheControl.Should().BeSameAs(cacheControl); + } + [Fact] public void CreateFromClass_WhenCalledWithToolWhoseNameIsNull_ItShouldThrowException() { @@ -306,6 +421,29 @@ public class ToolTests : SerializationTest t => t.IgnoringCyclicReferences() ); } + + [Fact] + public void CreateFromClass_WhenCalledWithProperToolAndCacheControl_ItShouldReturnTool() + { + var cacheControl = new EphemeralCacheControl(); + var tool = Tool.CreateFromClass(cacheControl); + + var expectedSchema = new JsonObject() + { + ["type"] = "object", + }; + + tool.Name.Should().Be("Name"); + tool.DisplayName.Should().Be("Name"); + tool.Description.Should().Be("Description"); + tool.Function.Method.Name.Should().Be(nameof(ProperTool.GetWeather)); + tool.Function.Instance.Should().BeOfType(); + tool.InputSchema.Should().BeEquivalentTo( + expectedSchema, + t => t.IgnoringCyclicReferences() + ); + tool.CacheControl.Should().BeSameAs(cacheControl); + } } class TestClass From f1c02ebc8faf4804a464d27c2a5d4c663cb82015 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:56:27 -0500 Subject: [PATCH 24/26] chore: whitelist word --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5d815ef..211b47a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "dotnet.defaultSolution": "AnthropicClient.sln", "cSpell.words": [ "Browsable", + "haikus", "nameof", "Szalay", "typeof" From c2fa796bd013d55d42468980fb9d9ac98f025264 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:56:52 -0500 Subject: [PATCH 25/26] docs: update README.md with documentation about using prompt caching with this library. --- README.md | 249 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 243 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4d11603..93ab829 100644 --- a/README.md +++ b/README.md @@ -431,10 +431,11 @@ if (response.IsSuccess is false) return; } +messages.Add(new(MessageRole.Assistant, response.Content)); + + foreach (var content in response.Value.Content) { - messages.Add(new(MessageRole.Assistant, [content])); - switch (content) { case TextContent textContent: @@ -552,10 +553,7 @@ if (response is null) return; } -foreach (var content in response.Content) -{ - messages.Add(new(MessageRole.Assistant, [content])); -} +messages.Add(new(MessageRole.Assistant, response.Content)); if (response?.ToolCall is not null) { @@ -610,3 +608,242 @@ foreach (var content in finalResponse.Value.Content) ``` If you do find that you need more control over how exactly provided tools are called and how the result of those tools are returned you can avoid using the `InvokeAsync` method and instead use the `Tool` and `ToolUse` properties of the `ToolCall` instance to implement your own solution. + +### System Prompt + +Anthropic's models support the use of system prompts to provide additional context to the user. This can be used to provide additional information to the user or to ask for additional information from the user. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/system-prompts). This library aims to make using system prompts convenient by allowing you to provide the system prompts you want Anthropic's models to consider for use when creating a message. + +#### System Message + +You can create a system prompt by providing a `string` as the `system` parameter in the `MessageRequest` or `StreamMessageRequest` constructor. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.CreateMessageAsync(new MessageRequest( + AnthropicModels.Claude3Haiku, + [ + new( + MessageRole.User, + [new TextContent("Please write a haiku about the ocean.")] + ) + ], + system: "You are a internationally renowned poet. You excel at writing haikus. +)); + +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; + } +} +``` + +#### System Messages + +You can create a more complex system prompt by providing a `List` as the `systemMessages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.CreateMessageAsync(new MessageRequest( + AnthropicModels.Claude3Haiku, + [ + new( + MessageRole.User, + [new TextContent("Please write a haiku about the ocean.")] + ) + ], + systemMessages: [ + new TextContent("You are a internationally renowned poet. You excel at writing haikus."), + new TextContent("You have been asked to write a haiku about the ocean.") + ] +)); + +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; + } +} +``` + +### Prompt Caching + +Anthropic has recently introduced a feature called [Prompt Caching](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) that allows you to cache all or part of the prompt you send to the model. This can be used to improve the performance of your application by reducing latency and token usage. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching). + +> [!NOTE] +> This feature is in beta and requires you to set an `anthropic-beta` header on your requests to use it. +> The value of the header should be `prompt-caching-2024-07-31`. + +When using this library you can opt-in to prompt caching by adding the required header to the `HttpClient` instance you provide to the `AnthropicApiClient` constructor. + +```csharp +using AnthropicClient; + +var httpClient = new HttpClient(); +httpClient.DefaultRequestHeaders.Add("anthropic-beta", "prompt-caching-2024-07-31"); + +var client = new AnthropicApiClient(apiKey, httpClient); +``` + +Prompt caching can be used to cache all parts of the prompt including system messages, user messages, and tools. You should refer to the [Anthropic API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching) for specifics on limitations and requirements for using prompt caching. This library aims to make using prompt caching convenient and give you complete control over what parts of the prompt are cached. Currently there is only one type of cache control available - `EphemeralCacheControl`. + +#### Caching System Messages + +System messages can be cached by providing a `List` as the `systemMessages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `TextContent` instances have the `CacheControl` property set. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.CreateMessageAsync(new MessageRequest( + AnthropicModels.Claude3Haiku, + [ + new( + MessageRole.User, + [new TextContent("Please write a haiku about the ocean.")] + ) + ], + systemMessages: [ + new TextContent("You are a internationally renowned poet. You excel at writing haikus. Please use the following as examples."), + new TextContent(exampleHaikus, new EphemeralCacheControl()) + ] +)); + +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; + } +} +``` + +#### Caching User Messages + +User messages can be cached by providing a `List` as the `messages` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `Content` instances have the `CacheControl` property set. + +```csharp +using AnthropicClient; +using AnthropicClient.Models; + +var response = await client.CreateMessageAsync(new MessageRequest( + AnthropicModels.Claude3Haiku, + [ + new( + MessageRole.User, + [ + new TextContent("Please write a haiku about the ocean. Here are some examples of haikus I like."), + new TextContent(exampleHaikus, new EphemeralCacheControl()) + ] + ), + ] +)); + +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; + } +} +``` + +#### Caching Tools + +Tools can be cached by providing a `List` as the `tools` parameter in the `MessageRequest` or `StreamMessageRequest` constructor and having one or more of the `Tool` instances have the `CacheControl` property set. This property can be set after the tool is created manually or by using one of the static methods on the `Tool` class. + +```csharp +using AnthropicClient; +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, + new EphemeralCacheControl() +); + +var response = await client.CreateMessageAsync(new MessageRequest( + AnthropicModels.Claude3Haiku, + [ + new( + MessageRole.User, + [new TextContent("What is the weather in New York?")] + ) + ], + tools: [ + // Lots of other 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; + } +} +``` From afe70a90125ab569e5c5b7dceeba2836807f84b3 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:59:47 -0500 Subject: [PATCH 26/26] chore: run dotnet format --- src/AnthropicClient/AnthropicApiClient.cs | 2 +- .../Models/BaseMessageRequest.cs | 2 +- src/AnthropicClient/Models/CacheControl.cs | 2 +- src/AnthropicClient/Models/Content.cs | 2 +- src/AnthropicClient/Models/TextContent.cs | 2 +- src/AnthropicClient/Models/Tool.cs | 24 +++++++++---------- .../Models/ToolResultContent.cs | 2 +- .../Unit/Models/MessageDeltaEventDataTests.cs | 18 +++++++------- .../Unit/Models/MessageRequestTests.cs | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 7ec3b10..15736f1 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -275,7 +275,7 @@ public class AnthropicApiClient : IAnthropicApiClient } private async Task SendRequestAsync(BaseMessageRequest request) - { + { var requestJson = Serialize(request); var requestContent = new StringContent(requestJson, Encoding.UTF8, JsonContentType); return await _httpClient.PostAsync(MessagesEndpoint, requestContent); diff --git a/src/AnthropicClient/Models/BaseMessageRequest.cs b/src/AnthropicClient/Models/BaseMessageRequest.cs index 040f743..04ac289 100644 --- a/src/AnthropicClient/Models/BaseMessageRequest.cs +++ b/src/AnthropicClient/Models/BaseMessageRequest.cs @@ -109,7 +109,7 @@ public abstract class BaseMessageRequest [JsonConstructor] internal BaseMessageRequest() { } - + /// /// Initializes a new instance of the class. /// diff --git a/src/AnthropicClient/Models/CacheControl.cs b/src/AnthropicClient/Models/CacheControl.cs index b39e487..28f0898 100644 --- a/src/AnthropicClient/Models/CacheControl.cs +++ b/src/AnthropicClient/Models/CacheControl.cs @@ -6,7 +6,7 @@ namespace AnthropicClient.Models; /// Represents the cache control to be used for content. /// public abstract class CacheControl -{ +{ /// /// Gets the type of the cache control. /// diff --git a/src/AnthropicClient/Models/Content.cs b/src/AnthropicClient/Models/Content.cs index a8aa0dd..efe3bac 100644 --- a/src/AnthropicClient/Models/Content.cs +++ b/src/AnthropicClient/Models/Content.cs @@ -11,7 +11,7 @@ public abstract class Content /// Gets the type of the content. /// public string Type { get; init; } = string.Empty; - + /// /// Gets the cache control to be used for the content. /// diff --git a/src/AnthropicClient/Models/TextContent.cs b/src/AnthropicClient/Models/TextContent.cs index b9827bc..a6751cf 100644 --- a/src/AnthropicClient/Models/TextContent.cs +++ b/src/AnthropicClient/Models/TextContent.cs @@ -47,7 +47,7 @@ public class TextContent : Content public TextContent(string text, CacheControl cacheControl) : base(ContentType.Text, cacheControl) { Validate(text); - + Text = text; } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/Tool.cs b/src/AnthropicClient/Models/Tool.cs index 66243fc..ab527ab 100644 --- a/src/AnthropicClient/Models/Tool.cs +++ b/src/AnthropicClient/Models/Tool.cs @@ -131,9 +131,9 @@ public class Tool /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. public static Tool CreateFromStaticMethod( - string name, - string description, - Type type, + string name, + string description, + Type type, string methodName, CacheControl? cacheControl = null ) @@ -165,9 +165,9 @@ public class Tool /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. public static Tool CreateFromInstanceMethod( - string name, - string description, - object instance, + string name, + string description, + object instance, string methodName, CacheControl? cacheControl = null ) @@ -197,8 +197,8 @@ public class Tool /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. public static Tool CreateFromFunction( - string name, - string description, + string name, + string description, Func func, CacheControl? cacheControl = null ) @@ -221,8 +221,8 @@ public class Tool /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. public static Tool CreateFromFunction( - string name, - string description, + string name, + string description, Func func, CacheControl? cacheControl = null ) @@ -246,8 +246,8 @@ public class Tool /// The created tool as instance of . /// The name of the tool will be sanitized to conform to the Anthropic tool naming rules. public static Tool CreateFromFunction( - string name, - string description, + string name, + string description, Func func, CacheControl? cacheControl = null ) diff --git a/src/AnthropicClient/Models/ToolResultContent.cs b/src/AnthropicClient/Models/ToolResultContent.cs index 634ac87..01312a7 100644 --- a/src/AnthropicClient/Models/ToolResultContent.cs +++ b/src/AnthropicClient/Models/ToolResultContent.cs @@ -44,7 +44,7 @@ public class ToolResultContent : Content Content = content; } - /// + /// /// Initializes a new instance of the class. /// /// The tool use ID of the content. diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs index b164915..b17db66 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageDeltaEventDataTests.cs @@ -20,9 +20,9 @@ public class MessageDeltaEventDataTests : SerializationTest public void Constructor_WhenCalled_ItShouldInitializeProperties() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage - { - InputTokens = 1, + var expectedUsage = new Usage + { + InputTokens = 1, OutputTokens = 1, CacheCreationInputTokens = 1, CacheReadInputTokens = 1, @@ -38,9 +38,9 @@ public class MessageDeltaEventDataTests : SerializationTest public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage - { - InputTokens = 1, + var expectedUsage = new Usage + { + InputTokens = 1, OutputTokens = 1, CacheCreationInputTokens = 1, CacheReadInputTokens = 1, @@ -57,9 +57,9 @@ public class MessageDeltaEventDataTests : SerializationTest public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues() { var expectedDelta = new MessageDelta("max_tokens", "max_tokens"); - var expectedUsage = new Usage - { - InputTokens = 1, + var expectedUsage = new Usage + { + InputTokens = 1, OutputTokens = 1, CacheCreationInputTokens = 1, CacheReadInputTokens = 1, diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs index 6f96c54..2efd402 100644 --- a/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs +++ b/tests/AnthropicClient.Tests/Unit/Models/MessageRequestTests.cs @@ -408,7 +408,7 @@ public class MessageRequestTests : SerializationTest JsonAssert.Equal(expected, actual); } - [Fact] + [Fact] public void JsonSerialization_WhenSerializedAndSystemMessageIsProvided_ItShouldUseSystemMessageForHaveSystemProperty() { var messageRequest = new MessageRequest(