Skip to content

Commit 5b3df4a

Browse files
Merge pull request #37 from StevanFreeborn/stevanfreeborn/feat/add-citations
feat: add citations support
2 parents d0c62cb + bee8fb1 commit 5b3df4a

43 files changed

Lines changed: 1599 additions & 70 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ insert_final_newline = false
2424
#### .NET Coding Conventions ####
2525
[*.{cs,vb}]
2626

27+
# diagnostics
28+
dotnet_diagnostic.IDE0058.severity = none
29+
dotnet_diagnostic.CA1707.severity = none
30+
2731
# Organize usings
2832
dotnet_separate_import_directive_groups = true
2933
dotnet_sort_system_directives_first = true
@@ -77,10 +81,13 @@ dotnet_remove_unnecessary_suppression_exclusions = none
7781
#### C# Coding Conventions ####
7882
[*.cs]
7983

84+
# namespace preferences
85+
csharp_style_namespace_declarations = file_scoped:suggestion
86+
8087
# var preferences
81-
csharp_style_var_elsewhere = false:silent
82-
csharp_style_var_for_built_in_types = false:silent
83-
csharp_style_var_when_type_is_apparent = false:silent
88+
csharp_style_var_elsewhere = true:suggestion
89+
csharp_style_var_for_built_in_types = true:suggestion
90+
csharp_style_var_when_type_is_apparent = true:suggestion
8491

8592
# Expression-bodied members
8693
csharp_style_expression_bodied_accessors = true:silent
@@ -118,7 +125,7 @@ csharp_style_pattern_local_over_anonymous_function = true:suggestion
118125
csharp_style_prefer_index_operator = true:suggestion
119126
csharp_style_prefer_range_operator = true:suggestion
120127
csharp_style_throw_expression = true:suggestion
121-
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
128+
csharp_style_unused_value_assignment_preference = discard_variable:silent
122129
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
123130

124131
# 'using' directive preferences

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@
1717
"targetdir",
1818
"typeof"
1919
],
20-
"dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings"
20+
"dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings",
21+
"search.exclude": {
22+
"**/docs": true,
23+
}
2124
}

README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,192 @@ foreach (var content in response.Value.Content)
972972
}
973973
```
974974

975+
### Citations
976+
977+
Anthropic provides a feature called [Citations](https://docs.anthropic.com/en/docs/build-with-claude/citations) that allows Claude to provide citations for information extracted from documents. This feature enables Claude to reference specific parts of the source material when answering questions, making it easier to verify information and understand the context of responses.
978+
979+
Citations can be enabled for documents and will return references to the specific locations in the source material where information was found. This library provides comprehensive support for citations through strongly-typed models that represent different types of citation locations.
980+
981+
#### Enabling Citations for Documents
982+
983+
You can enable citations for documents by setting the `Citations` property on `DocumentContent` instances:
984+
985+
```csharp
986+
using AnthropicClient;
987+
using AnthropicClient.Models;
988+
989+
var request = new MessageRequest(
990+
model: AnthropicModels.Claude35Sonnet,
991+
messages: [
992+
new(MessageRole.User, [
993+
new DocumentContent(new TextSource("The grass is green. The sky is blue."))
994+
{
995+
Title = "My Document",
996+
Context = "This is a trustworthy document.",
997+
Citations = new() { Enabled = true }
998+
},
999+
new TextContent("What color is the grass and sky?")
1000+
])
1001+
]
1002+
);
1003+
1004+
var response = await client.CreateMessageAsync(request);
1005+
1006+
if (response.IsSuccess is false)
1007+
{
1008+
Console.WriteLine("Failed to create message");
1009+
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
1010+
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
1011+
return;
1012+
}
1013+
1014+
foreach (var content in response.Value.Content)
1015+
{
1016+
switch (content)
1017+
{
1018+
case TextContent textContent:
1019+
Console.WriteLine("Response: {0}", textContent.Text);
1020+
1021+
if (textContent.Citations is not null)
1022+
{
1023+
Console.WriteLine("Citations:");
1024+
foreach (var citation in textContent.Citations)
1025+
{
1026+
Console.WriteLine(" - Cited Text: {0}", citation.CitedText);
1027+
Console.WriteLine(" Document: {0}", citation.DocumentTitle);
1028+
Console.WriteLine(" Type: {0}", citation.Type);
1029+
1030+
switch (citation)
1031+
{
1032+
case CharacterLocationCitation charCitation:
1033+
Console.WriteLine(
1034+
" Character Range: {0}-{1}",
1035+
charCitation.StartCharIndex, charCitation.EndCharIndex
1036+
);
1037+
break;
1038+
case PageLocationCitation pageCitation:
1039+
Console.WriteLine(
1040+
" Page Range: {0}-{1}",
1041+
pageCitation.StartPageNumber, pageCitation.EndPageNumber
1042+
);
1043+
break;
1044+
case ContentBlockLocationCitation blockCitation:
1045+
Console.WriteLine(
1046+
" Block Range: {0}-{1}",
1047+
blockCitation.StartBlockIndex, blockCitation.EndBlockIndex
1048+
);
1049+
break;
1050+
}
1051+
}
1052+
}
1053+
break;
1054+
}
1055+
}
1056+
```
1057+
1058+
#### Citations with PDF Documents
1059+
1060+
Citations work particularly well with PDF documents, providing page-level references:
1061+
1062+
```csharp
1063+
using AnthropicClient;
1064+
using AnthropicClient.Models;
1065+
1066+
var pdfBytes = await File.ReadAllBytesAsync("document.pdf");
1067+
var base64Data = Convert.ToBase64String(pdfBytes);
1068+
1069+
var request = new MessageRequest(
1070+
model: AnthropicModels.Claude35Sonnet,
1071+
messages: [
1072+
new(MessageRole.User, [
1073+
new DocumentContent("application/pdf", base64Data)
1074+
{
1075+
Title = "Research Paper",
1076+
Citations = new() { Enabled = true }
1077+
},
1078+
new TextContent("Summarize the key findings from this research paper.")
1079+
])
1080+
]
1081+
);
1082+
1083+
var response = await client.CreateMessageAsync(request);
1084+
1085+
if (response.IsSuccess is false)
1086+
{
1087+
Console.WriteLine("Failed to create message");
1088+
Console.WriteLine("Error Type: {0}", response.Error.Error.Type);
1089+
Console.WriteLine("Error Message: {0}", response.Error.Error.Message);
1090+
return;
1091+
}
1092+
1093+
foreach (var content in response.Value.Content)
1094+
{
1095+
switch (content)
1096+
{
1097+
case TextContent textContent:
1098+
Console.WriteLine("Summary: {0}", textContent.Text);
1099+
1100+
if (textContent.Citations is not null)
1101+
{
1102+
Console.WriteLine("\nCitations:");
1103+
foreach (var citation in textContent.Citations.OfType<PageLocationCitation>())
1104+
{
1105+
Console.WriteLine(
1106+
" - \"{0}\" (Pages {1}-{2})",
1107+
citation.CitedText,
1108+
citation.StartPageNumber,
1109+
citation.EndPageNumber
1110+
);
1111+
}
1112+
}
1113+
break;
1114+
}
1115+
}
1116+
```
1117+
1118+
#### Citations in Streaming Responses
1119+
1120+
Citations are also supported in streaming responses through the `CitationDelta` events:
1121+
1122+
```csharp
1123+
using AnthropicClient;
1124+
using AnthropicClient.Models;
1125+
1126+
var request = new StreamMessageRequest(
1127+
model: AnthropicModels.Claude35Sonnet,
1128+
messages: [
1129+
new(MessageRole.User, [
1130+
new DocumentContent(new TextSource("The grass is green. The sky is blue."))
1131+
{
1132+
Citations = new() { Enabled = true }
1133+
},
1134+
new TextContent("What color is the grass?")
1135+
])
1136+
]
1137+
);
1138+
1139+
var events = client.CreateMessageAsync(request);
1140+
1141+
await foreach (var e in events)
1142+
{
1143+
switch (e.Data)
1144+
{
1145+
case ContentDeltaEventData contentData:
1146+
switch (contentData.Delta)
1147+
{
1148+
case CitationDelta citationDelta:
1149+
Console.WriteLine("Citation: {0}", citationDelta.Citation.CitedText);
1150+
Console.WriteLine("Type: {0}", citationDelta.Citation.Type);
1151+
break;
1152+
case TextDelta textDelta:
1153+
Console.Write(textDelta.Text);
1154+
break;
1155+
}
1156+
break;
1157+
}
1158+
}
1159+
```
1160+
9751161
### Message Batches
9761162

9771163
Anthropic provides a feature called [Message Batches](https://docs.anthropic.com/en/docs/build-with-claude/message-batches) that allows you to send multiple messages in a single request. This feature is covered in depth in [Anthropic's API Documentation](https://docs.anthropic.com/en/docs/build-with-claude/message-batches).

src/AnthropicClient/AnthropicApiClient.cs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,37 @@ public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRe
124124
// current content type and delta type
125125
if (currentEvent.Type is EventType.ContentBlockDelta && currentEvent.Data is ContentDeltaEventData contentDeltaData)
126126
{
127-
if (content is TextContent textContent && contentDeltaData.Delta is TextDelta textDelta)
127+
if (content is TextContent textContent)
128128
{
129-
var newText = textContent.Text + textDelta.Text;
130-
content = new TextContent(newText);
129+
if (contentDeltaData.Delta is TextDelta textDelta)
130+
{
131+
var newText = textContent.Text + textDelta.Text;
132+
133+
content = new TextContent(newText)
134+
{
135+
Citations = textContent.Citations,
136+
};
137+
}
138+
139+
if (contentDeltaData.Delta is CitationDelta citationDelta)
140+
{
141+
var citations = new List<Citation>()
142+
{
143+
citationDelta.Citation,
144+
};
145+
146+
if (textContent.Citations is not null)
147+
{
148+
citations.AddRange(textContent.Citations);
149+
}
150+
151+
var newContent = new TextContent(textContent.Text)
152+
{
153+
Citations = [.. citations],
154+
};
155+
156+
content = newContent;
157+
}
131158
}
132159

133160
if (content is ToolUseContent toolUseContent && contentDeltaData.Delta is JsonDelta jsonDelta)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
using AnthropicClient.Models;
5+
6+
namespace AnthropicClient.Json;
7+
8+
class CitationConverter : JsonConverter<Citation>
9+
{
10+
public override Citation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11+
{
12+
using var jsonDocument = JsonDocument.ParseValue(ref reader);
13+
var root = jsonDocument.RootElement;
14+
var type = root.GetProperty("type").GetString();
15+
return type switch
16+
{
17+
CitationType.CharacterLocation => JsonSerializer.Deserialize<CharacterLocationCitation>(root.GetRawText(), options)!,
18+
CitationType.PageLocation => JsonSerializer.Deserialize<PageLocationCitation>(root.GetRawText(), options)!,
19+
CitationType.ContentBlockLocation => JsonSerializer.Deserialize<ContentBlockLocationCitation>(root.GetRawText(), options)!,
20+
_ => throw new JsonException($"Unknown citation type: {type}")
21+
};
22+
}
23+
24+
public override void Write(Utf8JsonWriter writer, Citation value, JsonSerializerOptions options)
25+
{
26+
JsonSerializer.Serialize(writer, value, value.GetType(), options);
27+
}
28+
}

src/AnthropicClient/Json/ContentDeltaConverter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public override ContentDelta Read(ref Utf8JsonReader reader, Type typeToConvert,
1616
{
1717
ContentDeltaType.TextDelta => JsonSerializer.Deserialize<TextDelta>(root.GetRawText(), options)!,
1818
ContentDeltaType.JsonDelta => JsonSerializer.Deserialize<JsonDelta>(root.GetRawText(), options)!,
19+
ContentDeltaType.CitationDelta => JsonSerializer.Deserialize<CitationDelta>(root.GetRawText(), options)!,
1920
_ => throw new JsonException($"Unknown content type: {type}")
2021
};
2122
}

src/AnthropicClient/Json/JsonSerializationOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ static class JsonSerializationOptions
1818
new ContentDeltaConverter(),
1919
new JsonStringEnumConverter(),
2020
new MessageBatchResultConverter(),
21+
new CitationConverter(),
22+
new SourceConverter(),
2123
},
2224
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
2325
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
using AnthropicClient.Models;
5+
6+
namespace AnthropicClient.Json;
7+
8+
class SourceConverter : JsonConverter<Source>
9+
{
10+
public override Source Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
11+
{
12+
using var jsonDocument = JsonDocument.ParseValue(ref reader);
13+
var root = jsonDocument.RootElement;
14+
var type = root.GetProperty("type").GetString();
15+
return type switch
16+
{
17+
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
18+
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
19+
SourceType.Base64 => DeserializeBase64Source(root, options),
20+
_ => throw new JsonException($"Unknown source type: {type}")
21+
};
22+
}
23+
24+
private static Source DeserializeBase64Source(JsonElement root, JsonSerializerOptions options)
25+
{
26+
var mediaType = root.TryGetProperty("media_type", out var mediaTypeElement)
27+
? mediaTypeElement.GetString() ?? throw new JsonException("Missing 'media_type' property")
28+
: throw new JsonException("Missing 'media_type' property");
29+
30+
var isImage = ImageType.IsValidImageType(mediaType);
31+
32+
return isImage
33+
? JsonSerializer.Deserialize<ImageSource>(root.GetRawText(), options)!
34+
: JsonSerializer.Deserialize<DocumentSource>(root.GetRawText(), options)!;
35+
}
36+
37+
public override void Write(Utf8JsonWriter writer, Source value, JsonSerializerOptions options)
38+
{
39+
if (value is TextSource textSource)
40+
{
41+
JsonSerializer.Serialize(writer, textSource, options);
42+
return;
43+
}
44+
45+
if (value is CustomSource customSource)
46+
{
47+
JsonSerializer.Serialize(writer, customSource, options);
48+
return;
49+
}
50+
51+
if (value is Base64Source base64Source)
52+
{
53+
JsonSerializer.Serialize(writer, base64Source, options);
54+
return;
55+
}
56+
57+
JsonSerializer.Serialize(writer, value, value.GetType(), options);
58+
}
59+
}

0 commit comments

Comments
 (0)