feat: add support for citations when streaming
This commit is contained in:
@@ -124,10 +124,37 @@ public class AnthropicApiClient : IAnthropicApiClient
|
|||||||
// current content type and delta type
|
// current content type and delta type
|
||||||
if (currentEvent.Type is EventType.ContentBlockDelta && currentEvent.Data is ContentDeltaEventData contentDeltaData)
|
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)
|
||||||
|
{
|
||||||
|
if (contentDeltaData.Delta is TextDelta textDelta)
|
||||||
{
|
{
|
||||||
var newText = textContent.Text + textDelta.Text;
|
var newText = textContent.Text + textDelta.Text;
|
||||||
content = new TextContent(newText);
|
|
||||||
|
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)
|
if (content is ToolUseContent toolUseContent && contentDeltaData.Delta is JsonDelta jsonDelta)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ContentDeltaConverter : JsonConverter<ContentDelta>
|
|||||||
{
|
{
|
||||||
ContentDeltaType.TextDelta => JsonSerializer.Deserialize<TextDelta>(root.GetRawText(), options)!,
|
ContentDeltaType.TextDelta => JsonSerializer.Deserialize<TextDelta>(root.GetRawText(), options)!,
|
||||||
ContentDeltaType.JsonDelta => JsonSerializer.Deserialize<JsonDelta>(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}")
|
_ => throw new JsonException($"Unknown content type: {type}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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>
|
||||||
|
public CitationDelta(Citation citation) : base(ContentDeltaType.CitationDelta)
|
||||||
|
{
|
||||||
|
ArgumentValidator.ThrowIfNull(citation, nameof(citation));
|
||||||
|
Citation = citation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,4 +14,9 @@ public static class ContentDeltaType
|
|||||||
/// The input_json_delta.
|
/// The input_json_delta.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string JsonDelta = "input_json_delta";
|
public const string JsonDelta = "input_json_delta";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The citation_delta.
|
||||||
|
/// </summary>
|
||||||
|
public const string CitationDelta = "citations_delta";
|
||||||
}
|
}
|
||||||
@@ -316,10 +316,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
var citations = result.Value
|
var citations = result.Value
|
||||||
.Content
|
.Content
|
||||||
.OfType<TextContent>()
|
.OfType<TextContent>()
|
||||||
.SelectMany(static c =>
|
.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
{
|
|
||||||
return c.Citations is null ? [] : c.Citations;
|
|
||||||
});
|
|
||||||
|
|
||||||
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||||
}
|
}
|
||||||
@@ -398,6 +395,127 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse()
|
||||||
|
{
|
||||||
|
var request = new StreamMessageRequest(
|
||||||
|
model: AnthropicModels.Claude35HaikuLatest,
|
||||||
|
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 result = _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
var messageCompleteEvent = await result
|
||||||
|
.Where(e => e.Type is EventType.MessageComplete)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var citations = messageCompleteEvent.Data
|
||||||
|
.As<MessageCompleteEventData>()
|
||||||
|
.Message
|
||||||
|
.Content
|
||||||
|
.OfType<TextContent>()
|
||||||
|
.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
|
|
||||||
|
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForPDFDocumentSource_ItShouldReturnCitationsInResponse()
|
||||||
|
{
|
||||||
|
var pdfPath = TestFileHelper.GetTestFilePath("addendum.pdf");
|
||||||
|
var bytes = await File.ReadAllBytesAsync(pdfPath);
|
||||||
|
var base64Data = Convert.ToBase64String(bytes);
|
||||||
|
|
||||||
|
var request = new StreamMessageRequest(
|
||||||
|
model: AnthropicModels.Claude35HaikuLatest,
|
||||||
|
messages: [
|
||||||
|
new(
|
||||||
|
MessageRole.User,
|
||||||
|
[
|
||||||
|
new DocumentContent("application/pdf", base64Data)
|
||||||
|
{
|
||||||
|
Title = "My PDF Document",
|
||||||
|
Context = "This is a trustworthy document.",
|
||||||
|
Citations = new() { Enabled = true }
|
||||||
|
},
|
||||||
|
new TextContent("What is the title of this paper?"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
var messageCompleteEvent = await result
|
||||||
|
.Where(e => e.Type is EventType.MessageComplete)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var citations = messageCompleteEvent.Data
|
||||||
|
.As<MessageCompleteEventData>()
|
||||||
|
.Message
|
||||||
|
.Content
|
||||||
|
.OfType<TextContent>()
|
||||||
|
.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
|
|
||||||
|
citations.OfType<PageLocationCitation>().Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForCustomDocumentSource_ItShouldReturnCitationsInResponse()
|
||||||
|
{
|
||||||
|
var request = new StreamMessageRequest(
|
||||||
|
model: AnthropicModels.Claude35HaikuLatest,
|
||||||
|
messages: [
|
||||||
|
new(
|
||||||
|
MessageRole.User,
|
||||||
|
[
|
||||||
|
new DocumentContent(
|
||||||
|
new CustomSource([
|
||||||
|
new TextContent("The grass is green. The sky is blue.")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Title = "My Custom Document",
|
||||||
|
Context = "This is a trustworthy document.",
|
||||||
|
Citations = new() { Enabled = true }
|
||||||
|
},
|
||||||
|
new TextContent("What color is the grass and sky?"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
var messageCompleteEvent = await result
|
||||||
|
.Where(e => e.Type is EventType.MessageComplete)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var citations = messageCompleteEvent.Data
|
||||||
|
.As<MessageCompleteEventData>()
|
||||||
|
.Message
|
||||||
|
.Content
|
||||||
|
.OfType<TextContent>()
|
||||||
|
.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
|
|
||||||
|
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
|
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user