diff --git a/Anthropic.SDK.Tests/CacheControlTests.cs b/Anthropic.SDK.Tests/CacheControlTests.cs index 39cb883..6719b29 100644 --- a/Anthropic.SDK.Tests/CacheControlTests.cs +++ b/Anthropic.SDK.Tests/CacheControlTests.cs @@ -129,7 +129,7 @@ public async Task TestSingleToolAllowsCacheIndependentOfToolSize() Stream = false, Temperature = 1.0m, System = systemMessages, - PromptCaching = PromptCacheType.Messages, + PromptCaching = PromptCacheType.AutomaticToolsAndSystem, Tools = tools }; var res = await client.Messages.GetClaudeMessageAsync(parameters); @@ -223,7 +223,7 @@ public async Task TestCacheControlStreaming() Stream = true, Temperature = 1.0m, System = systemMessages, - PromptCaching = PromptCacheType.Messages + PromptCaching = PromptCacheType.AutomaticToolsAndSystem }; var messageResponses = new List(); await foreach (var message in client.Messages.StreamClaudeMessageAsync(parameters)) diff --git a/Anthropic.SDK.Tests/DocumentTests.cs b/Anthropic.SDK.Tests/DocumentTests.cs new file mode 100644 index 0000000..dea21e3 --- /dev/null +++ b/Anthropic.SDK.Tests/DocumentTests.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class DocumentTests + { + [TestMethod] + public async Task TestPDF() + { + string resourceName = "Anthropic.SDK.Tests.Claude3ModelCard.pdf"; + + Assembly assembly = Assembly.GetExecutingAssembly(); + + await using Stream stream = assembly.GetManifestResourceStream(resourceName); + //read stream into byte array + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + byte[] pdfBytes = ms.ToArray(); + string base64String = Convert.ToBase64String(pdfBytes); + + + var client = new AnthropicClient(); + var messages = new List() + { + new Message(RoleType.User, new DocumentContent() + { + Source = new DocumentSource() + { + Type = SourceType.base64, + Data = base64String, + MediaType = "application/pdf" + }, + CacheControl = new CacheControl() + { + Type = CacheControlType.ephemeral + } + }), + new Message(RoleType.User, "Which model has the highest human preference win rates across each use-case?"), + }; + + var parameters = new MessageParameters() + { + Messages = messages, + MaxTokens = 1024, + Model = AnthropicModels.Claude35Sonnet, + Stream = false, + Temperature = 0m, + PromptCaching = PromptCacheType.FineGrained + }; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsNotNull(res.FirstMessage.ToString()); + Assert.IsTrue(res.Usage.CacheCreationInputTokens > 0 || res.Usage.CacheReadInputTokens > 0); + + + } + + [TestMethod] + public async Task TestPDFCitations() + { + var client = new AnthropicClient(); + var messages = new List() + { + new Message(RoleType.User, new DocumentContent() + { + Source = new DocumentSource() + { + Type = SourceType.url, + Url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" + }, + CacheControl = new CacheControl() + { + Type = CacheControlType.ephemeral + }, + Citations = new Citations() { Enabled = true } + }), + new Message(RoleType.User, "What are the key findings in this document? Use citations to back up your answer."), + }; + + var parameters = new MessageParameters() + { + Messages = messages, + MaxTokens = 1024, + Model = AnthropicModels.Claude35Sonnet, + Stream = false, + Temperature = 0m, + PromptCaching = PromptCacheType.FineGrained + }; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsTrue(res.Content.SelectMany(p => (p as TextContent).Citations ?? new List()).Any()); + + + } + + [TestMethod] + public async Task TestPDFCitationsStreaming() + { + var client = new AnthropicClient(); + var messages = new List() + { + new Message(RoleType.User, new DocumentContent() + { + Source = new DocumentSource() + { + Type = SourceType.url, + Url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" + }, + CacheControl = new CacheControl() + { + Type = CacheControlType.ephemeral + }, + Citations = new Citations() { Enabled = true } + }), + new Message(RoleType.User, "What are the key findings in this document? Use citations to back up your answer."), + }; + + var parameters = new MessageParameters() + { + Messages = messages, + MaxTokens = 1024, + Model = AnthropicModels.Claude35Sonnet, + Stream = false, + Temperature = 0m, + PromptCaching = PromptCacheType.FineGrained + }; + var responses = new List(); + await foreach (var result in client.Messages.StreamClaudeMessageAsync(parameters)) + { + responses.Add(result); + } + + var message = new Message(responses); + Assert.IsTrue(message.Content.SelectMany(p => (p as TextContent).Citations ?? new List()).Any()); + + } + + [TestMethod] + public async Task TestDocumentCitations() + { + string resourceName = "Anthropic.SDK.Tests.BillyBudd.txt"; + + Assembly assembly = Assembly.GetExecutingAssembly(); + + await using Stream stream = assembly.GetManifestResourceStream(resourceName); + using StreamReader reader = new StreamReader(stream); + string content = await reader.ReadToEndAsync(); + + + var client = new AnthropicClient(); + var messages = new List() + { + new Message(RoleType.User, new DocumentContent() + { + Source = new DocumentSource() + { + Type = SourceType.content, + Content = [new TextContent() + { + Text = content + }] + }, + Citations = new Citations() { Enabled = true } + }), + new Message(RoleType.User, "Who is the protagonist in this text? Use citations to back up your answer."), + }; + + var parameters = new MessageParameters() + { + Messages = messages, + MaxTokens = 1024, + Model = AnthropicModels.Claude35Sonnet, + Stream = false, + Temperature = 0m + }; + var res = await client.Messages.GetClaudeMessageAsync(parameters); + + Assert.IsTrue(res.Content.SelectMany(p => (p as TextContent).Citations ?? new List()).Any()); + + + } + + + } + + +} diff --git a/Anthropic.SDK.Tests/ModelTests.cs b/Anthropic.SDK.Tests/ModelTests.cs new file mode 100644 index 0000000..c5d5843 --- /dev/null +++ b/Anthropic.SDK.Tests/ModelTests.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class ModelTests + { + [TestMethod] + public async Task TestModelEndpointFunctionality() + { + var client = new AnthropicClient(); + var res = await client.Models.ListModelsAsync(); + Assert.IsNotNull(res.Models); + var modelId = res.Models.First().Id; + var model = await client.Models.GetModelAsync(modelId); + Assert.IsNotNull(model); + } + } +} diff --git a/Anthropic.SDK.Tests/PDFTests.cs b/Anthropic.SDK.Tests/PDFTests.cs deleted file mode 100644 index 5cdeb13..0000000 --- a/Anthropic.SDK.Tests/PDFTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Anthropic.SDK.Constants; -using Anthropic.SDK.Messaging; - -namespace Anthropic.SDK.Tests -{ - [TestClass] - public class PDFTests - { - [TestMethod] - public async Task TestPDF() - { - string resourceName = "Anthropic.SDK.Tests.Claude3ModelCard.pdf"; - - Assembly assembly = Assembly.GetExecutingAssembly(); - - await using Stream stream = assembly.GetManifestResourceStream(resourceName); - //read stream into byte array - using var ms = new MemoryStream(); - await stream.CopyToAsync(ms); - byte[] pdfBytes = ms.ToArray(); - string base64String = Convert.ToBase64String(pdfBytes); - - - var client = new AnthropicClient(); - var messages = new List() - { - new Message(RoleType.User, new DocumentContent() - { - Source = new ImageSource() - { - Data = base64String, - MediaType = "application/pdf" - }, - CacheControl = new CacheControl() - { - Type = CacheControlType.ephemeral - } - }), - new Message(RoleType.User, "Which model has the highest human preference win rates across each use-case?"), - }; - - var parameters = new MessageParameters() - { - Messages = messages, - MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, - Stream = false, - Temperature = 0m, - PromptCaching = PromptCacheType.FineGrained - }; - var res = await client.Messages.GetClaudeMessageAsync(parameters); - - Debug.WriteLine(res.Message); - Assert.IsTrue(res.Usage.CacheCreationInputTokens > 0 || res.Usage.CacheReadInputTokens > 0); - - - } - } -} diff --git a/Anthropic.SDK.Tests/VisionTests.cs b/Anthropic.SDK.Tests/VisionTests.cs new file mode 100644 index 0000000..d47da62 --- /dev/null +++ b/Anthropic.SDK.Tests/VisionTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; + +namespace Anthropic.SDK.Tests +{ + [TestClass] + public class VisionTests + { + [TestMethod] + public async Task TestVisionUrl() + { + var client = new AnthropicClient(); + + var mp = new MessageParameters() + { + Model = AnthropicModels.Claude37Sonnet, + MaxTokens = 1024, + Messages = new List() + { + new Message() + { + Content = new List() + { + new ImageContent() + { + Source = new ImageSource() + { + Type = SourceType.url, + Url = + "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg" + } + }, + new TextContent() + { + Text = "Describe this image." + } + } + } + } + + + }; + var res = await client.Messages.GetClaudeMessageAsync(mp); + Assert.IsNotNull(res.FirstMessage.ToString()); + } + } +} diff --git a/Anthropic.SDK/Anthropic.SDK.csproj b/Anthropic.SDK/Anthropic.SDK.csproj index bf7f779..f33e9b4 100644 --- a/Anthropic.SDK/Anthropic.SDK.csproj +++ b/Anthropic.SDK/Anthropic.SDK.csproj @@ -14,12 +14,12 @@ Claude, AI, ML, API, Anthropic Claude API - Updates Microsoft.Extensions.AI.Abstractions + Support for Citations, More Expansive Document Support, and Better Caching Support Anthropic.SDK - 4.7.2 - 4.7.2.0 - 4.7.2.0 + 5.0.0 + 5.0.0.0 + 5.0.0.0 True README.md icon.png diff --git a/Anthropic.SDK/AnthropicClient.cs b/Anthropic.SDK/AnthropicClient.cs index 2a797e7..e6f3392 100644 --- a/Anthropic.SDK/AnthropicClient.cs +++ b/Anthropic.SDK/AnthropicClient.cs @@ -4,11 +4,18 @@ using System.Text.Json.Serialization; using System.Text.Json; using Anthropic.SDK.Batches; +using Anthropic.SDK.Models; namespace Anthropic.SDK { + /// + /// Entry point to the Anthropic API, handling auth and allowing access to the various API endpoints + /// public class AnthropicClient : IDisposable { + /// + /// The base URL for the API + /// public string ApiUrlFormat { get; set; } = "https://api.anthropic.com/{0}/{1}"; /// @@ -57,6 +64,7 @@ public AnthropicClient(APIAuthentication apiKeys = null, HttpClient client = nul this.Auth = apiKeys.ThisOrDefault(); Messages = new MessagesEndpoint(this); Batches = new BatchesEndpoint(this); + Models = new ModelsEndpoint(this); } internal static JsonSerializerOptions JsonSerializationOptions { get; } = new() @@ -81,7 +89,7 @@ private HttpClient SetupClient(HttpClient client) }); #else return new HttpClient(); - #endif +#endif } ~AnthropicClient() @@ -94,12 +102,23 @@ private HttpClient SetupClient(HttpClient client) /// public MessagesEndpoint Messages { get; } + /// + /// Batches are a way to send multiple requests to the API at once. This is useful for when you have a large number of requests to make, or when you want to make multiple requests in parallel. + /// public BatchesEndpoint Batches { get; } + /// + /// Models are a way to manage the models that the API uses to generate completions. You can list models, as well as get information about a specific model. + /// + public ModelsEndpoint Models { get; } + #region IDisposable private bool isDisposed; + /// + /// Disposes of the resources used by the . + /// public void Dispose() { Dispose(true); diff --git a/Anthropic.SDK/Batches/BatchesEndpoint.cs b/Anthropic.SDK/Batches/BatchesEndpoint.cs index 327a60a..6da5a8a 100644 --- a/Anthropic.SDK/Batches/BatchesEndpoint.cs +++ b/Anthropic.SDK/Batches/BatchesEndpoint.cs @@ -10,7 +10,7 @@ namespace Anthropic.SDK.Batches public class BatchesEndpoint : EndpointBase { /// - /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . /// /// internal BatchesEndpoint(AnthropicClient client) : base(client) { } @@ -24,7 +24,7 @@ internal BatchesEndpoint(AnthropicClient client) : base(client) { } /// public async Task CreateBatchAsync(List batches, CancellationToken ctx = default) { - var response = await HttpRequestBatches(Url, HttpMethod.Post, new { requests = batches }, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url, HttpMethod.Post, new { requests = batches }, ctx).ConfigureAwait(false); return response; } @@ -36,7 +36,7 @@ public async Task CreateBatchAsync(List batches, Ca /// public async Task CancelBatchAsync(string batchId, CancellationToken ctx = default) { - var response = await HttpRequestBatches(Url + $"/{batchId}/cancel", HttpMethod.Post, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url + $"/{batchId}/cancel", HttpMethod.Post, null, ctx).ConfigureAwait(false); return response; } @@ -48,7 +48,7 @@ public async Task CancelBatchAsync(string batchId, CancellationTo /// public async Task RetrieveBatchStatusAsync(string batchId, CancellationToken ctx = default) { - var response = await HttpRequestBatches(Url + $"/{batchId}", HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(Url + $"/{batchId}", HttpMethod.Get, null, ctx).ConfigureAwait(false); return response; } @@ -104,7 +104,7 @@ public async Task ListBatchesAsync(string beforeId = null, string aft url += $"&after_id={afterId}"; } - var response = await HttpRequestBatchesList(url, HttpMethod.Get, null, ctx).ConfigureAwait(false); + var response = await HttpRequestSimple(url, HttpMethod.Get, null, ctx).ConfigureAwait(false); return response; } diff --git a/Anthropic.SDK/EndpointBase.cs b/Anthropic.SDK/EndpointBase.cs index db7eb19..398de2c 100644 --- a/Anthropic.SDK/EndpointBase.cs +++ b/Anthropic.SDK/EndpointBase.cs @@ -129,25 +129,14 @@ protected async Task HttpRequestMessages(string url = null return res; } - protected async Task HttpRequestBatches(string url = null, HttpMethod verb = null, + protected async Task HttpRequestSimple(string url = null, HttpMethod verb = null, object postData = null, CancellationToken ctx = default) { var response = await HttpRequestRaw(url, verb, postData, false, ctx).ConfigureAwait(false); string resultAsString = await ReadResponseContentAsync(response, ctx).ConfigureAwait(false); using var ms = new MemoryStream(Encoding.UTF8.GetBytes(resultAsString)); - var res = await JsonSerializer.DeserializeAsync(ms, cancellationToken: ctx).ConfigureAwait(false); - return res; - } - - protected async Task HttpRequestBatchesList(string url = null, HttpMethod verb = null, - object postData = null, CancellationToken ctx = default) - { - var response = await HttpRequestRaw(url, verb, postData, false, ctx).ConfigureAwait(false); - string resultAsString = await ReadResponseContentAsync(response, ctx).ConfigureAwait(false); - - using var ms = new MemoryStream(Encoding.UTF8.GetBytes(resultAsString)); - var res = await JsonSerializer.DeserializeAsync(ms, cancellationToken: ctx).ConfigureAwait(false); + var res = await JsonSerializer.DeserializeAsync(ms, cancellationToken: ctx).ConfigureAwait(false); return res; } diff --git a/Anthropic.SDK/Messaging/Content.cs b/Anthropic.SDK/Messaging/Content.cs index 279bca9..a73dce6 100644 --- a/Anthropic.SDK/Messaging/Content.cs +++ b/Anthropic.SDK/Messaging/Content.cs @@ -41,11 +41,43 @@ public class TextContent: ContentBase [JsonPropertyName("text")] public string Text { get; set; } + /// + /// Citations + /// + [JsonPropertyName("citations")] + public List Citations { get; set; } + public override string ToString() => Text?.ToString() ?? string.Empty; public static implicit operator string(TextContent choice) => choice?.ToString(); } + public class CitationResult + { + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("cited_text")] + public string CitedText { get; set; } + [JsonPropertyName("document_index")] + public int DocumentIndex { get; set; } + [JsonPropertyName("document_title")] + public string DocumentTitle { get; set; } + [JsonPropertyName("start_char_index")] + public long? StartCharIndex { get; set; } + [JsonPropertyName("end_char_index")] + public long? EndCharIndex { get; set; } + [JsonPropertyName("start_page_number")] + public long? StartPageNumber { get; set; } + [JsonPropertyName("end_page_number")] + public long? EndPageNumber { get; set; } + [JsonPropertyName("start_block_index")] + public long? StartBlockIndex { get; set; } + [JsonPropertyName("end_block_index")] + public long? EndBlockIndex { get; set; } + + + } + /// /// Helper Class for Thinking Content /// @@ -118,22 +150,88 @@ public class DocumentContent : ContentBase /// Source of Document /// [JsonPropertyName("source")] - public ImageSource Source { get; set; } + public DocumentSource Source { get; set; } + + /// + /// Citations + /// + [JsonPropertyName("citations")] + public Citations Citations { get; set; } + + /// + /// Context + /// + [JsonPropertyName("context")] + public string Context { get; set; } + + /// + /// Title + /// + [JsonPropertyName("title")] + public string Title { get; set; } + } + + /// + /// Helper Class for Citations + /// + public class Citations + { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } } /// /// Image/Document Format Types /// - public static class ImageSourceType + public enum SourceType + { + base64, + text, + url, + content + } + + /// + /// Definition of document to be sent to Claude + /// + public class DocumentSource { /// - /// Base 64 Image Type + /// Image data format (pre-set) + /// + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SourceType Type { get; set; } + + /// + /// Content of the Document + /// + [JsonPropertyName("content")] + public List Content { get; set; } + + /// + /// Image format + /// + [JsonPropertyName("media_type")] + public string MediaType { get; set; } + + /// + /// Base 64 image data + /// + [JsonPropertyName("data")] + public string Data { get; set; } + + /// + /// Document URL /// - public static string Base64 => "base64"; + [JsonPropertyName("url")] + public string Url { get; set; } } + + /// - /// Definition of image/document to be sent to Claude + /// Definition of image to be sent to Claude /// public class ImageSource { @@ -141,7 +239,8 @@ public class ImageSource /// Image data format (pre-set) /// [JsonPropertyName("type")] - public string Type => ImageSourceType.Base64; + [JsonConverter(typeof(JsonStringEnumConverter))] + public SourceType Type { get; set; } /// /// Image format @@ -149,6 +248,12 @@ public class ImageSource [JsonPropertyName("media_type")] public string MediaType { get; set; } + /// + /// Image URL + /// + [JsonPropertyName("url")] + public string Url { get; set; } + /// /// Base 64 image data /// diff --git a/Anthropic.SDK/Messaging/Message.cs b/Anthropic.SDK/Messaging/Message.cs index 2deec7f..5ff0bed 100644 --- a/Anthropic.SDK/Messaging/Message.cs +++ b/Anthropic.SDK/Messaging/Message.cs @@ -67,7 +67,7 @@ public Message(Function toolCall, string data, string mediaType, bool isError = public Message(List asyncResponses) { - Content = new(); + Content = []; var arguments = string.Empty; var text = string.Empty; var thinking = string.Empty; @@ -112,23 +112,44 @@ public Message(List asyncResponses) }); } - + var innerText = string.Empty; + CitationResult citation = null; foreach (var result in asyncResponses) { - if (!string.IsNullOrWhiteSpace(result.Delta?.Text)) + if ((result.Type != "content_block_stop")) { - text += result.Delta.Text; - } + if (result.Delta?.Type == "text_delta") + { + innerText += result.Delta?.Text ?? string.Empty; + } - } - if (!string.IsNullOrWhiteSpace(text)) - { - Content.Add(new TextContent() + citation ??= result.Delta?.Citation; + } + else if (result.Type == "content_block_stop") { - Text = text - }); + if (!string.IsNullOrEmpty(innerText)) + { + Content.Add(new TextContent() + { + Text = innerText, + Citations = citation != null ? [citation] : null + }); + } + + innerText = string.Empty; + citation = null; + } + } + //if (!string.IsNullOrEmpty(innerText)) + //{ + // Content.Add(new TextContent() + // { + // Text = innerText + // }); + //} + diff --git a/Anthropic.SDK/Messaging/MessageResponse.cs b/Anthropic.SDK/Messaging/MessageResponse.cs index ec19618..94a0210 100644 --- a/Anthropic.SDK/Messaging/MessageResponse.cs +++ b/Anthropic.SDK/Messaging/MessageResponse.cs @@ -117,6 +117,8 @@ public class Delta [JsonPropertyName("partial_json")] public string? PartialJson { get; set; } + [JsonPropertyName("citation")] + public CitationResult Citation { get; set; } } public class ContentBlock diff --git a/Anthropic.SDK/Messaging/MessagesEndpoint.cs b/Anthropic.SDK/Messaging/MessagesEndpoint.cs index d3fc5e1..c6b6ad1 100644 --- a/Anthropic.SDK/Messaging/MessagesEndpoint.cs +++ b/Anthropic.SDK/Messaging/MessagesEndpoint.cs @@ -12,7 +12,7 @@ namespace Anthropic.SDK.Messaging public partial class MessagesEndpoint : EndpointBase { /// - /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . /// /// internal MessagesEndpoint(AnthropicClient client) : base(client) { } @@ -54,50 +54,30 @@ public async Task GetClaudeMessageAsync(MessageParameters param private static void SetCacheControls(MessageParameters parameters) { - if ( (parameters.PromptCaching & PromptCacheType.FineGrained) == PromptCacheType.FineGrained) + if (parameters.PromptCaching == PromptCacheType.FineGrained) { // just use each one's cache control, assume they are already set } - else + else if (parameters.PromptCaching == PromptCacheType.AutomaticToolsAndSystem) { - bool hasMessageCaching = (parameters.PromptCaching & PromptCacheType.Messages) == PromptCacheType.Messages; - bool hasToolCaching = (parameters.PromptCaching & PromptCacheType.Tools) == PromptCacheType.Tools; - if (hasMessageCaching && parameters.System != null && parameters.System.Any()) + + if (parameters.System != null && parameters.System.Any()) { parameters.System.Last().CacheControl = new CacheControl() { Type = CacheControlType.ephemeral }; } - - if (hasToolCaching && parameters.Tools != null && parameters.Tools.Any()) + + if (parameters.Tools != null && parameters.Tools.Any()) { parameters.Tools.Last().Function.CacheControl = new CacheControl() { Type = CacheControlType.ephemeral }; } + - var count = parameters.Messages.Count(p => p.Role == RoleType.User); - if (hasMessageCaching && parameters.Messages != null && count > 1) - { - var x = 1; - foreach (var message in parameters.Messages.Where(p => p.Role == RoleType.User)) - { - if (x < count - 2) - { - message.Content.ForEach(p => p.CacheControl = null); - } - else - { - message.Content.Last().CacheControl = new CacheControl() { - Type = CacheControlType.ephemeral - }; - } - - x++; - } - } } } @@ -148,7 +128,12 @@ public async IAsyncEnumerable StreamClaudeMessageAsync(MessageP yield return result; } } - + /// + /// Makes a call to count the number of tokens in a request. + /// + /// + /// + /// public async Task CountMessageTokensAsync(MessageCountTokenParameters parameters, CancellationToken ctx = default) { return await HttpRequestMessages($"{Url}/count_tokens", HttpMethod.Post, parameters, ctx).ConfigureAwait(false); diff --git a/Anthropic.SDK/Messaging/PromptCacheType.cs b/Anthropic.SDK/Messaging/PromptCacheType.cs index 0ff523e..2545e59 100644 --- a/Anthropic.SDK/Messaging/PromptCacheType.cs +++ b/Anthropic.SDK/Messaging/PromptCacheType.cs @@ -3,7 +3,7 @@ namespace Anthropic.SDK.Messaging; /// -/// Prompt Cache Type Definitions. Designed to be used as a bitwise assignment if you want to cache multiple types and are caching enough context. +/// Prompt Cache Type Definitions. /// [Flags] public enum PromptCacheType @@ -13,15 +13,11 @@ public enum PromptCacheType /// None = 0, /// - /// Cache System and User Messages + /// Use the cache-control instructions from each message for fine-grained control /// - Messages = 1 << 0, // 1 + FineGrained = 1, /// - /// Cache Tool Definitions + /// Use the cache-control instructions from the system messages for automatic tools and system message caching /// - Tools = 1 << 1, // 2 - /// - /// Use the cache-control instructions from each message - /// - FineGrained = 1 << 2, // 4 + AutomaticToolsAndSystem = 2, } \ No newline at end of file diff --git a/Anthropic.SDK/Models/ModelList.cs b/Anthropic.SDK/Models/ModelList.cs new file mode 100644 index 0000000..ba11e75 --- /dev/null +++ b/Anthropic.SDK/Models/ModelList.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Anthropic.SDK.Models +{ + public class ModelList + { + [JsonPropertyName("data")] + public List Models { get; set; } + + [JsonPropertyName("has_more")] + public bool HasMore { get; set; } + + [JsonPropertyName("first_id")] + public string FirstId { get; set; } + + [JsonPropertyName("last_id")] + public string LastId { get; set; } + } + + +} diff --git a/Anthropic.SDK/Models/ModelResponse.cs b/Anthropic.SDK/Models/ModelResponse.cs new file mode 100644 index 0000000..70351a3 --- /dev/null +++ b/Anthropic.SDK/Models/ModelResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Anthropic.SDK.Models +{ + public class ModelResponse + { + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } + [JsonPropertyName("display_name")] + public string DisplayName { get; set; } + [JsonPropertyName("id")] + public string Id { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + } +} diff --git a/Anthropic.SDK/Models/ModelsEndpoint.cs b/Anthropic.SDK/Models/ModelsEndpoint.cs new file mode 100644 index 0000000..e17a8a5 --- /dev/null +++ b/Anthropic.SDK/Models/ModelsEndpoint.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace Anthropic.SDK.Models +{ + /// + /// Endpoint for interacting with the Models API. + /// + public class ModelsEndpoint: EndpointBase + { + + /// + /// Constructor of the api endpoint. Rather than instantiating this yourself, access it through an instance of as . + /// + /// + internal ModelsEndpoint(AnthropicClient client) : base(client) { } + + protected override string Endpoint => "models"; + + /// + /// Retrieves a paginated list of Models from the Claude AI API. + /// + /// + /// + /// + /// + public async Task ListModelsAsync(string beforeId = null, string afterId = null, int limit = 20, CancellationToken ctx = default) + { + var url = Url + $"?limit={limit}"; + if (!string.IsNullOrEmpty(beforeId)) + { + url += $"&before_id={beforeId}"; + } + if (!string.IsNullOrEmpty(afterId)) + { + url += $"&after_id={afterId}"; + } + + var response = await HttpRequestSimple(url, HttpMethod.Get, null, ctx).ConfigureAwait(false); + + return response; + } + + /// + /// Makes a call to retrieve a specific model from the Claude AI API. + /// + /// + /// + public async Task GetModelAsync(string modelId, CancellationToken ctx = default) + { + var response = await HttpRequestSimple(Url + $"/{modelId}", HttpMethod.Get, null, ctx).ConfigureAwait(false); + + return response; + } + } +} diff --git a/README.md b/README.md index 1a50975..1548b3d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ Anthropic.SDK is an unofficial C# client designed for interacting with the Claud - [Extended Thinking](#extended-thinking) - [IChatClient](#ichatclient) - [Prompt Caching](#prompt-caching) - - [PDF Support](#pdf-support) + - [Document Support](#document-support) + - [Citations](#citations) + - [List Models](#list-models) - [Batching](#batching) - [Tools](#tools) - [Computer Use](#computer-use) @@ -317,7 +319,7 @@ Please see the unit tests for even more examples. ### Prompt Caching -The `AnthropicClient` supports prompt caching of system messages, user messages (including images), assistant messages, tool_results, and tools in accordance with model limitations. Because the `AnthropicClient` does not have it's own tokenizer, you must ensure yourself that when enabling prompt caching, you are providing enough context to the qualifying model for it to cache or nothing will be cached. Check out the documentation on Anthropic's website for specific model limitations and requirements. +The `AnthropicClient` supports prompt caching of system messages, user messages (including images), assistant messages, tool_results, documents, and tools in accordance with model limitations. There are two primary mechanisms for prompt caching in the `AnthropicClient`. `FineGrained` and `AutomaticToolsAndSystem`. The former allows for complete control of all set-points of caching (up to the 4 set-points allowed) where-as `AutomaticToolsAndSystem` automatically caches the System prompt and Tools when present, leaving you the ability to add set-points in Messages yourself when you so choose. When caching, be aware of the 5 minute expiry enforced by Anthropic, as well as other limitations that can cause a cache miss. You can check the Token Usage data in results to ensure you are indeed receiving the benefits of caching. ```csharp //load up a long form text you want to cache and ask questions of @@ -350,8 +352,8 @@ var parameters = new MessageParameters() Stream = false, Temperature = 1.0m, System = systemMessages, - //Key ingredient: we tell Claude we want it to cache messages - PromptCaching = PromptCacheType.Messages + //Key ingredient: we tell Claude we want it to cache any tools and system messages + PromptCaching = PromptCacheType.AutomaticToolsAndSystem }; var res = await client.Messages.GetClaudeMessageAsync(parameters); @@ -369,24 +371,7 @@ var res2 = await client.Messages.GetClaudeMessageAsync(parameters); Console.WriteLine(res2.Usage.CacheReadInputTokens); ``` -To cache tools (if you have a LOT of tools registered) and cache messages at the same time, you can simply declare the prompt caching type as a bitwise operation like so: - -```csharp -var parameters = new MessageParameters() -{ - Messages = messages, - MaxTokens = 1024, - Model = AnthropicModels.Claude35Sonnet, - Stream = false, - Temperature = 1.0m, - //Set caching as enabled for both messages and tools - PromptCaching = PromptCacheType.Messages | PromptCacheType.Tools, - Tools = tools -}; -var res = await client.Messages.GetClaudeMessageAsync(parameters); -``` - -Additionally, there is a mode for fine-grained control of caching, where you manage the cache points yourself. Here, you declare the cache control setting at the message and tool level, giving you complete control. +As mentioned, there is a mode for fine-grained control of caching, where you manage the cache points yourself. Here, you declare the cache control setting at the message and tool level, giving you complete control. Just remember that only 4 cache points can be set in total. ```csharp string resourceName = "Anthropic.SDK.Tests.BillyBudd.txt"; @@ -440,8 +425,8 @@ Console.WriteLine(res2.Usage.CacheReadInputTokens); See unit tests for additional examples. -### PDF Support -The `AnthropicClient` supports the new PDF Upload mechanism enabled by Claude. +### Document Support +The `AnthropicClient` supports the new PDF Upload mechanism enabled by Claude as well as other document types. ```csharp string resourceName = "Anthropic.SDK.Tests.Claude3ModelCard.pdf"; @@ -461,8 +446,9 @@ var messages = new List() { new Message(RoleType.User, new DocumentContent() { - Source = new ImageSource() + Source = new DocumentSource() { + Type = SourceType.base64 Data = base64String, MediaType = "application/pdf" }, @@ -488,9 +474,59 @@ var res = await client.Messages.GetClaudeMessageAsync(parameters); Console.WriteLine(res.Message); ``` +See Integration tests for more examples of other document types. + +### Citations + +The `AnthropicClient` supports Citations from the Claude API. Both non-streaming and streaming are supported. + +```csharp +var client = new AnthropicClient(); +var messages = new List() +{ + new Message(RoleType.User, new DocumentContent() + { + Source = new DocumentSource() + { + Type = SourceType.url, + Url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" + }, + Citations = new Citations() { Enabled = true } + }), + new Message(RoleType.User, "What are the key findings in this document? Use citations to back up your answer."), +}; + +var parameters = new MessageParameters() +{ + Messages = messages, + MaxTokens = 1024, + Model = AnthropicModels.Claude35Sonnet, + Stream = false, + Temperature = 0m +}; +var res = await client.Messages.GetClaudeMessageAsync(parameters); + +Assert.IsTrue(res.Content.SelectMany(p => (p as TextContent).Citations ?? new List()).Any()); +``` + +See integration tests for more examples like streaming. + +### List Models + +The `AnthropicClient` supports the Models API. + +```csharp +var client = new AnthropicClient(); +var res = await client.Models.ListModelsAsync(); +Assert.IsNotNull(res.Models); +var modelId = res.Models.First().Id; +var model = await client.Models.GetModelAsync(modelId); +Assert.IsNotNull(model); +``` + ### Batching -The `AnthropicClient` supports the new batching API. Abbreviated call examples are listed below, please check the `Anthropic.SDK.BatchTester` project for a more comprehensive example. +The `AnthropicClient` supports the batching API. Abbreviated call examples are listed below, please check the `Anthropic.SDK.BatchTester` project for a more comprehensive example. ```csharp //list batches