feat: initial pass at citations in request and response

This commit is contained in:
Stevan Freeborn
2025-06-10 23:34:33 -05:00
parent 8326e1749d
commit a9d0dfdc76
7 changed files with 146 additions and 0 deletions
@@ -0,0 +1,28 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AnthropicClient.Models;
namespace AnthropicClient.Json;
class CitationConverter : JsonConverter<Citation>
{
public override Citation 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
{
CitationType.CharacterLocation => JsonSerializer.Deserialize<CharacterLocationCitation>(root.GetRawText(), options)!,
CitationType.PageLocation => JsonSerializer.Deserialize<PageLocationCitation>(root.GetRawText(), options)!,
CitationType.ContentBlockLocation => JsonSerializer.Deserialize<ContentBlockLocationCitation>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown content type: {type}")
};
}
public override void Write(Utf8JsonWriter writer, Citation value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -18,6 +18,7 @@ static class JsonSerializationOptions
new ContentDeltaConverter(),
new JsonStringEnumConverter(),
new MessageBatchResultConverter(),
new CitationConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
@@ -0,0 +1,8 @@
namespace AnthropicClient.Models;
public static class CitationType
{
public const string CharacterLocation = "char_location";
public const string PageLocation = "page_location";
public const string ContentBlockLocation = "content_block_location";
}
@@ -14,6 +14,12 @@ public class DocumentContent : Content
/// </summary>
public DocumentSource Source { get; init; } = new();
public string Title { get; init; } = string.Empty;
public string Context { get; init; } = string.Empty;
public CitationOption Citations { get; init; } = new CitationOption();
[JsonConstructor]
internal DocumentContent()
{
@@ -53,4 +59,24 @@ public class DocumentContent : Content
Source = new(mediaType, data);
}
public DocumentContent(DocumentSource source) : base(ContentType.Document)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
Source = source;
}
public DocumentContent(DocumentSource source, CacheControl cacheControl) : base(ContentType.Document, cacheControl)
{
ArgumentValidator.ThrowIfNull(source, nameof(source));
Source = source;
}
}
public class CitationOption
{
public bool Enabled { get; init; }
}
+43
View File
@@ -14,6 +14,8 @@ public class TextContent : Content
/// </summary>
public string Text { get; init; } = string.Empty;
public Citation[] Citations { get; init; } = [];
[JsonConstructor]
internal TextContent() : base(ContentType.Text)
{
@@ -51,3 +53,44 @@ 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; }
}
@@ -0,0 +1,11 @@
namespace AnthropicClient.Models;
public class TextDocumentSource : DocumentSource
{
public TextDocumentSource(string data) : base("text/plain", data)
{
Type = "text";
}
}
@@ -286,6 +286,35 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
resultTwo.Value.Usage.CacheReadInputTokens.Should().BeGreaterThan(0);
}
[Fact]
public async Task CreateMessageAsync_WhenCitationsAreEnabled_ItShouldReturnCitationsInResponse()
{
var request = new MessageRequest(
model: AnthropicModels.Claude3Haiku,
messages: [
new(
MessageRole.User,
[
new DocumentContent(
new TextDocumentSource("The grass is green. The sky is blue.")
)
{
Title = "My Document",
Context = "This is a trustworthy document.",
Citations = new() { Enabled = true }
},
new TextContent("What color is the grass and sky?"),
]
)
]
);
var result = await _client.CreateMessageAsync(request);
result.IsSuccess.Should().BeTrue();
result.Value.Content.OfType<TextContent>().SelectMany(c => c.Citations).Should().NotBeEmpty();
}
[Fact]
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
{