Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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,
}
}
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 content 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 content 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);
}
}
39 changes: 39 additions & 0 deletions src/AnthropicClient/Models/Base64Source.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 29 additions & 0 deletions src/AnthropicClient/Models/CharacterLocationCitation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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; }

/// <summary>
/// Initializes a new instance of the <see cref="CharacterLocationCitation"/> class.
/// </summary>
/// <returns>A new instance of <see cref="CharacterLocationCitation"/>.</returns>
public CharacterLocationCitation() : base(CitationType.CharacterLocation)
{
}
}
42 changes: 42 additions & 0 deletions src/AnthropicClient/Models/Citation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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;

/// <summary>
/// Initializes a new instance of the <see cref="Citation"/> class with a specified type.
/// </summary>
/// <param name="type">The type of the citation.</param>
/// <returns>A new instance of <see cref="Citation"/>.</returns>
protected Citation(string type)
{
Type = type;
}
}
33 changes: 33 additions & 0 deletions src/AnthropicClient/Models/CitationDelta.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;

using AnthropicClient.Utils;

namespace AnthropicClient.Models;

/// <summary>
/// Represents a citation delta.
/// </summary>
public class CitationDelta : ContentDelta
{
/// <summary>
/// Gets the citation associated with this delta.
/// </summary>
public Citation Citation { get; init; } = new CharacterLocationCitation();

[JsonConstructor]
internal CitationDelta() : base(ContentDeltaType.CitationDelta)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="CitationDelta"/> class.
/// </summary>
/// <param name="citation">The citation to associate with this delta.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="citation"/> is null.</exception>
/// <returns>A new instance of <see cref="CitationDelta"/>.</returns>
public CitationDelta(Citation citation) : base(ContentDeltaType.CitationDelta)
{
ArgumentValidator.ThrowIfNull(citation, nameof(citation));
Citation = citation;
}
}
12 changes: 12 additions & 0 deletions src/AnthropicClient/Models/CitationOption.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
22 changes: 22 additions & 0 deletions src/AnthropicClient/Models/CitationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +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";
}
Loading