feat: add support for citations

BREAKING CHANGE: Changed type of public Source properties.
Updated `Source` property on `DocumentContent` and `ImageContent`
types to use base `Source` type

- include support for citing custom sources
- include support for citing PDF sources
- include support for citing text sources
This commit is contained in:
Stevan Freeborn
2025-06-14 13:44:10 -05:00
parent d50989833e
commit 5b8d88a01d
21 changed files with 450 additions and 123 deletions
@@ -19,6 +19,7 @@ static class JsonSerializationOptions
new JsonStringEnumConverter(),
new MessageBatchResultConverter(),
new CitationConverter(),
new SourceConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
@@ -0,0 +1,46 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class SourceConverter : JsonConverter<Source>
{
public override Source Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var type = root.GetProperty("type").GetString();
return type switch
{
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
SourceType.Base64 => JsonSerializer.Deserialize<Base64Source>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown content type: {type}")
};
}
public override void Write(Utf8JsonWriter writer, Source value, JsonSerializerOptions options)
{
if (value is TextSource textSource)
{
JsonSerializer.Serialize(writer, textSource, options);
return;
}
if (value is CustomSource customSource)
{
JsonSerializer.Serialize(writer, customSource, options);
return;
}
if (value is Base64Source base64Source)
{
JsonSerializer.Serialize(writer, base64Source, options);
return;
}
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents an base64 source.
/// </summary>
public class Base64Source : Source
{
/// <summary>
/// Gets the media type of the source.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; init; } = string.Empty;
/// <summary>
/// Gets the data of the source.
/// </summary>
public string Data { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="Base64Source"/> class.
/// </summary>
/// <param name="mediaType">The media type of the source.</param>
/// <param name="data">The data of the source.</param>
/// <exception cref="ArgumentException">Thrown when the media type is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the media type or data is null.</exception>
/// <returns>A new instance of the <see cref="Base64Source"/> class.</returns>
public Base64Source(string mediaType, string data) : base(SourceType.Base64)
{
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
ArgumentValidator.ThrowIfNull(data, nameof(data));
MediaType = mediaType;
Data = data;
}
}
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a citation for specific locations within text content.
/// </summary>
public class CharacterLocationCitation : Citation
{
/// <summary>
/// Gets the start character index of the citation.
/// </summary>
[JsonPropertyName("start_char_index")]
public int StartCharIndex { get; init; }
/// <summary>
/// Gets the end character index of the citation.
/// </summary>
[JsonPropertyName("end_char_index")]
public int EndCharIndex { get; init; }
}
+32
View File
@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a citation
/// </summary>
public abstract class Citation
{
/// <summary>
/// Gets the type of the citation.
/// </summary>
public string Type { get; init; } = string.Empty;
/// <summary>
/// Gets the text that is cited.
/// </summary>
[JsonPropertyName("cited_text")]
public string CitedText { get; init; } = string.Empty;
/// <summary>
/// Gets the document index of the citation.
/// </summary>
[JsonPropertyName("document_index")]
public int DocumentIndex { get; init; }
/// <summary>
/// Gets the title of the document from which the citation is made.
/// </summary>
[JsonPropertyName("document_title")]
public string DocumentTitle { get; init; } = string.Empty;
}
@@ -0,0 +1,12 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents whether citations are enabled for a document.
/// </summary>
public class CitationOption
{
/// <summary>
/// Gets a value indicating whether citations are enabled for the document.
/// </summary>
public bool Enabled { get; init; }
}
@@ -1,8 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// The types of citations that can be returned by the Anthropic API.
/// </summary>
public static class CitationType
{
/// <summary>
/// A citation that refers to a specific character in the text.
/// </summary>
public const string CharacterLocation = "char_location";
/// <summary>
/// A citation that refers to a specific page in the text.
/// </summary>
public const string PageLocation = "page_location";
/// <summary>
/// A citation that refers to a specific section in the text.
/// </summary>
public const string ContentBlockLocation = "content_block_location";
}
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a citation for content blocks within custom content.
/// </summary>
public class ContentBlockLocationCitation : Citation
{
/// <summary>
/// Gets the start block index of the citation.
/// /// </summary>
[JsonPropertyName("start_block_index")]
public int StartBlockIndex { get; init; }
/// <summary>
/// Gets the end block index of the citation.
/// </summary>
[JsonPropertyName("end_block_index")]
public int EndBlockIndex { get; init; }
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using AnthropicClient.Utils;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a custom source that contains a list of text content.
/// </summary>
public class CustomSource : Source
{
/// <summary>
/// Gets the list of text content that makes up the custom source.
/// </summary>
public List<TextContent> Content { get; init; } = [];
[JsonConstructor]
internal CustomSource() : base(SourceType.Content)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomSource"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="CustomSource"/> class.</returns>
/// <exception cref="ArgumentNullException">Thrown when the content is null.</exception>
public CustomSource(List<TextContent> content) : base(SourceType.Content)
{
ArgumentValidator.ThrowIfNull(content, nameof(content));
Content = content;
}
}
+30 -14
View File
@@ -12,13 +12,22 @@ public class DocumentContent : Content
/// <summary>
/// Gets the source of the document.
/// </summary>
public DocumentSource Source { get; init; } = new();
public Source Source { get; init; } = new DocumentSource();
public string Title { get; init; } = string.Empty;
/// <summary>
/// Gets the title of the document.
/// </summary>
public string? Title { get; init; }
public string Context { get; init; } = string.Empty;
/// <summary>
/// Gets the context of the document.
/// </summary>
public string? Context { get; init; }
public CitationOption Citations { get; init; } = new CitationOption();
/// <summary>
/// Gets whether citations are enabled for the document.
/// </summary>
public CitationOption? Citations { get; init; }
[JsonConstructor]
internal DocumentContent()
@@ -42,7 +51,7 @@ public class DocumentContent : Content
{
Validate(mediaType, data);
Source = new(mediaType, data);
Source = new DocumentSource(mediaType, data);
}
/// <summary>
@@ -57,26 +66,33 @@ public class DocumentContent : Content
{
Validate(mediaType, data);
Source = new(mediaType, data);
Source = new DocumentSource(mediaType, data);
}
public DocumentContent(DocumentSource source) : base(ContentType.Document)
/// <summary>
/// Initializes a new instance of the <see cref="DocumentContent"/> class with a document source.
/// </summary>
/// <param name="source">The document source.</param>
/// <returns>A new instance of the <see cref="DocumentContent"/> class.</returns>
/// <exception cref="ArgumentNullException">Thrown when the source is null.</exception>
public DocumentContent(Source source) : base(ContentType.Document)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
Source = source;
}
public DocumentContent(DocumentSource source, CacheControl cacheControl) : base(ContentType.Document, cacheControl)
/// <summary>
/// Initializes a new instance of the <see cref="DocumentContent"/> class with a document source and cache control.
/// </summary>
/// <param name="source">The document source.</param>
/// <param name="cacheControl">The cache control to be used for the content.</param>
/// <returns>A new instance of the <see cref="DocumentContent"/> class.</returns>
/// <exception cref="ArgumentNullException">Thrown when the source is null.</exception>
public DocumentContent(Source source, CacheControl cacheControl) : base(ContentType.Document, cacheControl)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
Source = source;
}
}
public class CitationOption
{
public bool Enabled { get; init; }
}
+3 -24
View File
@@ -7,26 +7,10 @@ namespace AnthropicClient.Models;
/// <summary>
/// Represents a document source.
/// </summary>
public class DocumentSource
public class DocumentSource : Base64Source
{
/// <summary>
/// Gets the media type of the document.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; init; } = string.Empty;
/// <summary>
/// Gets the data of the document.
/// </summary>
public string Data { get; init; } = string.Empty;
/// <summary>
/// Gets the type of encoding of the document data.
/// </summary>
public string Type { get; init; } = "base64";
[JsonConstructor]
internal DocumentSource()
internal DocumentSource() : base(string.Empty, string.Empty)
{
}
@@ -38,12 +22,7 @@ public class DocumentSource
/// <exception cref="ArgumentException">Thrown when the media type is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the media type or data is null.</exception>
/// <returns>A new instance of the <see cref="DocumentSource"/> class.</returns>
public DocumentSource(string mediaType, string data)
public DocumentSource(string mediaType, string data) : base(mediaType, data)
{
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
ArgumentValidator.ThrowIfNull(data, nameof(data));
MediaType = mediaType;
Data = data;
}
}
+3 -3
View File
@@ -12,7 +12,7 @@ public class ImageContent : Content
/// <summary>
/// Gets the source of the image.
/// </summary>
public ImageSource Source { get; init; } = new();
public Source Source { get; init; } = new ImageSource();
[JsonConstructor]
internal ImageContent()
@@ -36,7 +36,7 @@ public class ImageContent : Content
{
Validate(mediaType, data);
Source = new(mediaType, data);
Source = new ImageSource(mediaType, data);
}
/// <summary>
@@ -51,6 +51,6 @@ public class ImageContent : Content
{
Validate(mediaType, data);
Source = new(mediaType, data);
Source = new ImageSource(mediaType, data);
}
}
+3 -23
View File
@@ -7,26 +7,10 @@ namespace AnthropicClient.Models;
/// <summary>
/// Represents an image source.
/// </summary>
public class ImageSource
public class ImageSource : Base64Source
{
/// <summary>
/// Gets the media type of the image.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; init; } = string.Empty;
/// <summary>
/// Gets the data of the image.
/// </summary>
public string Data { get; init; } = string.Empty;
/// <summary>
/// Gets the type of encoding of the image data.
/// </summary>
public string Type { get; init; } = "base64";
[JsonConstructor]
internal ImageSource()
internal ImageSource() : base(string.Empty, string.Empty)
{
}
@@ -38,17 +22,13 @@ public class ImageSource
/// <exception cref="ArgumentException">Thrown when the media type is invalid.</exception>
/// <exception cref="ArgumentNullException">Thrown when the media type or data is null.</exception>
/// <returns>A new instance of the <see cref="ImageSource"/> class.</returns>
public ImageSource(string mediaType, string data)
public ImageSource(string mediaType, string data) : base(mediaType, data)
{
ArgumentValidator.ThrowIfNull(mediaType, nameof(mediaType));
if (ImageType.IsValidImageType(mediaType) is false)
{
throw new ArgumentException($"Invalid media type: {mediaType}");
}
ArgumentValidator.ThrowIfNull(data, nameof(data));
MediaType = mediaType;
Data = data;
}
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a citation for text within a page of a document.
/// </summary>
public class PageLocationCitation : Citation
{
/// <summary>
/// Gets the start page number of the citation.
/// </summary>
[JsonPropertyName("start_page_number")]
public int StartPageNumber { get; init; }
/// <summary>
/// Gets the end page number of the citation.
/// </summary>
[JsonPropertyName("end_page_number")]
public int EndPageNumber { get; init; }
}
+22
View File
@@ -0,0 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents a base class for sources.
/// </summary>
public abstract class Source
{
/// <summary>
/// Gets the type of the source.
/// </summary>
public string Type { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="Source"/> class.
/// </summary>
/// <param name="type">The type of the source.</param>
/// <returns>A new instance of the <see cref="Source"/> class.</returns>
protected Source(string type)
{
Type = type;
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the types of document sources that can be used in the Anthropic API.
/// </summary>
public static class SourceType
{
/// <summary>
/// The base64 encoded document source type.
/// </summary>
public const string Base64 = "base64";
/// <summary>
/// The custom content document source type.
/// </summary>
public const string Content = "content";
/// <summary>
/// The text document source type.
/// </summary>
public const string Text = "text";
}
+4 -42
View File
@@ -14,7 +14,10 @@ public class TextContent : Content
/// </summary>
public string Text { get; init; } = string.Empty;
public Citation[] Citations { get; init; } = [];
/// <summary>
/// Gets the citations associated with the text content.
/// </summary>
public Citation[]? Citations { get; init; }
[JsonConstructor]
internal TextContent() : base(ContentType.Text)
@@ -53,44 +56,3 @@ public class TextContent : Content
Text = text;
}
}
public abstract class Citation
{
public string Type { get; init; } = string.Empty;
[JsonPropertyName("cited_text")]
public string CitedText { get; init; } = string.Empty;
[JsonPropertyName("document_index")]
public int DocumentIndex { get; init; }
[JsonPropertyName("document_title")]
public string DocumentTitle { get; init; } = string.Empty;
}
public class CharacterLocationCitation : Citation
{
[JsonPropertyName("start_char_index")]
public int StartCharIndex { get; init; }
[JsonPropertyName("end_char_index")]
public int EndCharIndex { get; init; }
}
public class PageLocationCitation : Citation
{
[JsonPropertyName("start_page_number")]
public int StartPageNumber { get; init; }
[JsonPropertyName("end_page_number")]
public int EndPageNumber { get; init; }
}
public class ContentBlockLocationCitation : Citation
{
[JsonPropertyName("start_block_index")]
public int StartBlockIndex { get; init; }
[JsonPropertyName("end_block_index")]
public int EndBlockIndex { get; init; }
}
@@ -1,11 +0,0 @@
namespace AnthropicClient.Models;
public class TextDocumentSource : DocumentSource
{
public TextDocumentSource(string data) : base("text/plain", data)
{
Type = "text";
}
}
+33
View File
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
namespace AnthropicClient.Models;
/// <summary>
/// Represents a text document source.
/// </summary>
public class TextSource : Source
{
/// <summary>
/// Gets the media type of the source.
/// </summary>
[JsonPropertyName("media_type")]
public string MediaType { get; } = "text/plain";
/// <summary>
/// Gets the data of the source.
/// </summary>
public string Data { get; init; } = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="TextSource"/> class.
/// </summary>
/// <param name="data">The data of the document.</param>
/// <exception cref="ArgumentNullException">Thrown when the data is null.</exception>
/// <returns>A new instance of the <see cref="TextSource"/> class.</returns>
public TextSource(string data) : base(SourceType.Text)
{
Data = data;
}
}