Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ insert_final_newline = false
#### .NET Coding Conventions ####
[*.{cs,vb}]

# diagnostics
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.CA1707.severity = none

# Organize usings
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
Expand Down Expand Up @@ -77,10 +81,13 @@ dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]

# namespace preferences
csharp_style_namespace_declarations = file_scoped:suggestion

# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion

# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
Expand Down Expand Up @@ -118,7 +125,7 @@ csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:silent
csharp_style_unused_value_expression_statement_preference = discard_variable:silent

# 'using' directive preferences
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
"targetdir",
"typeof"
],
"dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings"
"dotnet.unitTests.runSettingsPath": "./tests/AnthropicClient.Tests/.runsettings",
"search.exclude": {
"**/docs": true,
}
}
186 changes: 186 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,192 @@ foreach (var content in response.Value.Content)
}
```

### Citations

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.

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.

#### Enabling Citations for Documents

You can enable citations for documents by setting the `Citations` property on `DocumentContent` instances:

```csharp
using AnthropicClient;
using AnthropicClient.Models;

var request = new MessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [
new DocumentContent(new TextSource("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 response = await client.CreateMessageAsync(request);

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("Response: {0}", textContent.Text);

if (textContent.Citations is not null)
{
Console.WriteLine("Citations:");
foreach (var citation in textContent.Citations)
{
Console.WriteLine(" - Cited Text: {0}", citation.CitedText);
Console.WriteLine(" Document: {0}", citation.DocumentTitle);
Console.WriteLine(" Type: {0}", citation.Type);

switch (citation)
{
case CharacterLocationCitation charCitation:
Console.WriteLine(
" Character Range: {0}-{1}",
charCitation.StartCharIndex, charCitation.EndCharIndex
);
break;
case PageLocationCitation pageCitation:
Console.WriteLine(
" Page Range: {0}-{1}",
pageCitation.StartPageNumber, pageCitation.EndPageNumber
);
break;
case ContentBlockLocationCitation blockCitation:
Console.WriteLine(
" Block Range: {0}-{1}",
blockCitation.StartBlockIndex, blockCitation.EndBlockIndex
);
break;
}
}
}
break;
}
}
```

#### Citations with PDF Documents

Citations work particularly well with PDF documents, providing page-level references:

```csharp
using AnthropicClient;
using AnthropicClient.Models;

var pdfBytes = await File.ReadAllBytesAsync("document.pdf");
var base64Data = Convert.ToBase64String(pdfBytes);

var request = new MessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [
new DocumentContent("application/pdf", base64Data)
{
Title = "Research Paper",
Citations = new() { Enabled = true }
},
new TextContent("Summarize the key findings from this research paper.")
])
]
);

var response = await client.CreateMessageAsync(request);

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("Summary: {0}", textContent.Text);

if (textContent.Citations is not null)
{
Console.WriteLine("\nCitations:");
foreach (var citation in textContent.Citations.OfType<PageLocationCitation>())
{
Console.WriteLine(
" - \"{0}\" (Pages {1}-{2})",
citation.CitedText,
citation.StartPageNumber,
citation.EndPageNumber
);
}
}
break;
}
}
```

#### Citations in Streaming Responses

Citations are also supported in streaming responses through the `CitationDelta` events:

```csharp
using AnthropicClient;
using AnthropicClient.Models;

var request = new StreamMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [
new DocumentContent(new TextSource("The grass is green. The sky is blue."))
{
Citations = new() { Enabled = true }
},
new TextContent("What color is the grass?")
])
]
);

var events = client.CreateMessageAsync(request);

await foreach (var e in events)
{
switch (e.Data)
{
case ContentDeltaEventData contentData:
switch (contentData.Delta)
{
case CitationDelta citationDelta:
Console.WriteLine("Citation: {0}", citationDelta.Citation.CitedText);
Console.WriteLine("Type: {0}", citationDelta.Citation.Type);
break;
case TextDelta textDelta:
Console.Write(textDelta.Text);
break;
}
break;
}
}
```

### Message Batches

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).
Expand Down
33 changes: 30 additions & 3 deletions src/AnthropicClient/AnthropicApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,37 @@ public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRe
// current content type and delta type
if (currentEvent.Type is EventType.ContentBlockDelta && currentEvent.Data is ContentDeltaEventData contentDeltaData)
{
if (content is TextContent textContent && contentDeltaData.Delta is TextDelta textDelta)
if (content is TextContent textContent)
{
var newText = textContent.Text + textDelta.Text;
content = new TextContent(newText);
if (contentDeltaData.Delta is TextDelta textDelta)
{
var newText = textContent.Text + textDelta.Text;

content = new TextContent(newText)
{
Citations = textContent.Citations,
};
}

if (contentDeltaData.Delta is CitationDelta citationDelta)
{
var citations = new List<Citation>()
{
citationDelta.Citation,
};

if (textContent.Citations is not null)
{
citations.AddRange(textContent.Citations);
}

var newContent = new TextContent(textContent.Text)
{
Citations = [.. citations],
};

content = newContent;
}
}

if (content is ToolUseContent toolUseContent && contentDeltaData.Delta is JsonDelta jsonDelta)
Expand Down
28 changes: 28 additions & 0 deletions src/AnthropicClient/Json/CitationConverter.cs
Original file line number Diff line number Diff line change
@@ -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 citation type: {type}")
};
}

public override void Write(Utf8JsonWriter writer, Citation value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
1 change: 1 addition & 0 deletions src/AnthropicClient/Json/ContentDeltaConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public override ContentDelta Read(ref Utf8JsonReader reader, Type typeToConvert,
{
ContentDeltaType.TextDelta => JsonSerializer.Deserialize<TextDelta>(root.GetRawText(), options)!,
ContentDeltaType.JsonDelta => JsonSerializer.Deserialize<JsonDelta>(root.GetRawText(), options)!,
ContentDeltaType.CitationDelta => JsonSerializer.Deserialize<CitationDelta>(root.GetRawText(), options)!,
_ => throw new JsonException($"Unknown content type: {type}")
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/AnthropicClient/Json/JsonSerializationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ static class JsonSerializationOptions
new ContentDeltaConverter(),
new JsonStringEnumConverter(),
new MessageBatchResultConverter(),
new CitationConverter(),
new SourceConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
Expand Down
59 changes: 59 additions & 0 deletions src/AnthropicClient/Json/SourceConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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 => DeserializeBase64Source(root, options),
_ => throw new JsonException($"Unknown source type: {type}")
};
}

private static Source DeserializeBase64Source(JsonElement root, JsonSerializerOptions options)
{
var mediaType = root.TryGetProperty("media_type", out var mediaTypeElement)
? mediaTypeElement.GetString() ?? throw new JsonException("Missing 'media_type' property")
: throw new JsonException("Missing 'media_type' property");

var isImage = ImageType.IsValidImageType(mediaType);

return isImage
? JsonSerializer.Deserialize<ImageSource>(root.GetRawText(), options)!
: JsonSerializer.Deserialize<DocumentSource>(root.GetRawText(), options)!;
}

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);
}
}
Loading