feat: add support for file source and url source
This commit is contained in:
@@ -16,6 +16,8 @@ class SourceConverter : JsonConverter<Source>
|
|||||||
{
|
{
|
||||||
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
|
SourceType.Text => JsonSerializer.Deserialize<TextSource>(root.GetRawText(), options)!,
|
||||||
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
|
SourceType.Content => JsonSerializer.Deserialize<CustomSource>(root.GetRawText(), options)!,
|
||||||
|
SourceType.File => JsonSerializer.Deserialize<FileSource>(root.GetRawText(), options)!,
|
||||||
|
SourceType.Url => JsonSerializer.Deserialize<UrlSource>(root.GetRawText(), options)!,
|
||||||
SourceType.Base64 => DeserializeBase64Source(root, options),
|
SourceType.Base64 => DeserializeBase64Source(root, options),
|
||||||
_ => throw new JsonException($"Unknown source type: {type}")
|
_ => throw new JsonException($"Unknown source type: {type}")
|
||||||
};
|
};
|
||||||
@@ -54,6 +56,18 @@ class SourceConverter : JsonConverter<Source>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value is FileSource fileSource)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, fileSource, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is UrlSource urlSource)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, urlSource, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
JsonSerializer.Serialize(writer, value, value.GetType(), options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a file source in the Anthropic API.
|
||||||
|
/// </summary>
|
||||||
|
public class FileSource : Source
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier for the file source.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("file_id")]
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FileSource"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new instance of <see cref="FileSource"/> with the type set to "file".</returns>
|
||||||
|
public FileSource() : base(SourceType.File)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FileSource"/> class with a specified file ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The unique identifier for the file source.</param>
|
||||||
|
/// <returns>A new instance of <see cref="FileSource"/>.</returns>
|
||||||
|
public FileSource(string id) : base(SourceType.File)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,4 +53,32 @@ public class ImageContent : Content
|
|||||||
|
|
||||||
Source = new ImageSource(mediaType, data);
|
Source = new ImageSource(mediaType, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the image.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the source is null.</exception>
|
||||||
|
public ImageContent(Source source) : base(ContentType.Image)
|
||||||
|
{
|
||||||
|
ArgumentValidator.ThrowIfNull(source, nameof(source));
|
||||||
|
|
||||||
|
Source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ImageContent"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The source of the image.</param>
|
||||||
|
/// <param name="cacheControl">The cache control to be used for the content.</param>
|
||||||
|
/// <returns>A new instance of the <see cref="ImageContent"/> class.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when the source or cache control is null.</exception>
|
||||||
|
public ImageContent(Source source, CacheControl cacheControl) : base(ContentType.Image, cacheControl)
|
||||||
|
{
|
||||||
|
ArgumentValidator.ThrowIfNull(source, nameof(source));
|
||||||
|
ArgumentValidator.ThrowIfNull(cacheControl, nameof(cacheControl));
|
||||||
|
|
||||||
|
Source = source;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,14 @@ public static class SourceType
|
|||||||
/// The text document source type.
|
/// The text document source type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Text = "text";
|
public const string Text = "text";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The file document source type.
|
||||||
|
/// </summary>
|
||||||
|
public const string File = "file";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL document source type.
|
||||||
|
/// </summary>
|
||||||
|
public const string Url = "url";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace AnthropicClient.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a URL source in the Anthropic API.
|
||||||
|
/// </summary>
|
||||||
|
public class UrlSource : Source
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the URL of the source document.
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UrlSource"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A new instance of <see cref="UrlSource"/> with the type set to "url".</returns>
|
||||||
|
public UrlSource() : base(SourceType.Url)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UrlSource"/> class with a specified URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">The URL of the source document.</param>
|
||||||
|
/// <returns>A new instance of <see cref="UrlSource"/>.</returns>
|
||||||
|
public UrlSource(string url) : base(SourceType.Url)
|
||||||
|
{
|
||||||
|
Url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,15 @@ using AnthropicClient.Tests.Files;
|
|||||||
|
|
||||||
namespace AnthropicClient.Tests.EndToEnd;
|
namespace AnthropicClient.Tests.EndToEnd;
|
||||||
|
|
||||||
public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
|
public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture), IAsyncLifetime
|
||||||
{
|
{
|
||||||
|
private readonly List<string> _filesToDelete = [];
|
||||||
|
|
||||||
|
public Task InitializeAsync()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
|
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
|
||||||
{
|
{
|
||||||
@@ -96,11 +103,41 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
text.Should().Contain("elephant");
|
text.Should().Contain("elephant");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenImageIsSentAsUrl_ItShouldReturnResponse()
|
||||||
|
{
|
||||||
|
var request = new MessageRequest(
|
||||||
|
model: AnthropicModels.Claude3Haiku,
|
||||||
|
messages: [
|
||||||
|
new(MessageRole.User, [
|
||||||
|
new ImageContent(new UrlSource("https://ftp.stevanfreeborn.com/share/anthropic-client/ant.jpg")),
|
||||||
|
new TextContent("What is in this image?")
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
result.Value.Should().BeOfType<MessageResponse>();
|
||||||
|
result.Value.Content.Should().NotBeNullOrEmpty();
|
||||||
|
|
||||||
|
var text = result.Value.Content.Aggregate("", static (acc, content) =>
|
||||||
|
{
|
||||||
|
if (content is TextContent textContent)
|
||||||
|
{
|
||||||
|
acc += textContent.Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
});
|
||||||
|
|
||||||
|
text.Should().Contain("ant");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache()
|
public async Task CreateMessageAsync_WhenSystemMessagesContainCacheControl_ItShouldUseCache()
|
||||||
{
|
{
|
||||||
var client = CreateClient(new HttpClient());
|
|
||||||
|
|
||||||
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
||||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||||
|
|
||||||
@@ -127,7 +164,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||||
|
|
||||||
var resultTwo = await client.CreateMessageAsync(request);
|
var resultTwo = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultTwo.IsSuccess.Should().BeTrue();
|
resultTwo.IsSuccess.Should().BeTrue();
|
||||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -138,8 +175,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache()
|
public async Task CreateMessageAsync_WhenMessagesContainCacheControl_ItShouldUseCache()
|
||||||
{
|
{
|
||||||
var client = CreateClient(new HttpClient());
|
|
||||||
|
|
||||||
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
var storyPath = TestFileHelper.GetTestFilePath("story.txt");
|
||||||
var storyText = await File.ReadAllTextAsync(storyPath);
|
var storyText = await File.ReadAllTextAsync(storyPath);
|
||||||
|
|
||||||
@@ -153,7 +188,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
var resultOne = await client.CreateMessageAsync(request);
|
var resultOne = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultOne.IsSuccess.Should().BeTrue();
|
resultOne.IsSuccess.Should().BeTrue();
|
||||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -163,7 +198,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this story?")]));
|
||||||
|
|
||||||
var resultTwo = await client.CreateMessageAsync(request);
|
var resultTwo = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultTwo.IsSuccess.Should().BeTrue();
|
resultTwo.IsSuccess.Should().BeTrue();
|
||||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -174,8 +209,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache()
|
public async Task CreateMessageAsync_WhenToolsContainCacheControl_ItShouldUseCache()
|
||||||
{
|
{
|
||||||
var client = CreateClient(new HttpClient());
|
|
||||||
|
|
||||||
var func = (string ticker) => ticker;
|
var func = (string ticker) => ticker;
|
||||||
|
|
||||||
var tools = Enumerable
|
var tools = Enumerable
|
||||||
@@ -195,7 +228,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
tools: tools
|
tools: tools
|
||||||
);
|
);
|
||||||
|
|
||||||
var resultOne = await client.CreateMessageAsync(request);
|
var resultOne = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultOne.IsSuccess.Should().BeTrue();
|
resultOne.IsSuccess.Should().BeTrue();
|
||||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -205,7 +238,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||||
request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")]));
|
request.Messages.Add(new(MessageRole.User, [new TextContent("Could you tell me the stock price for AAPL?")]));
|
||||||
|
|
||||||
var resultTwo = await client.CreateMessageAsync(request);
|
var resultTwo = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultTwo.IsSuccess.Should().BeTrue();
|
resultTwo.IsSuccess.Should().BeTrue();
|
||||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -228,9 +261,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
var client = CreateClient(new HttpClient());
|
var result = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
var result = await client.CreateMessageAsync(request);
|
|
||||||
|
|
||||||
result.IsSuccess.Should().BeTrue();
|
result.IsSuccess.Should().BeTrue();
|
||||||
result.Value.Should().BeOfType<MessageResponse>();
|
result.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -256,8 +287,6 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
var bytes = await File.ReadAllBytesAsync(pdfPath);
|
var bytes = await File.ReadAllBytesAsync(pdfPath);
|
||||||
var base64Data = Convert.ToBase64String(bytes);
|
var base64Data = Convert.ToBase64String(bytes);
|
||||||
|
|
||||||
var client = CreateClient(new HttpClient());
|
|
||||||
|
|
||||||
var request = new MessageRequest(
|
var request = new MessageRequest(
|
||||||
model: AnthropicModels.Claude35Sonnet,
|
model: AnthropicModels.Claude35Sonnet,
|
||||||
messages: [
|
messages: [
|
||||||
@@ -268,7 +297,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
var resultOne = await client.CreateMessageAsync(request);
|
var resultOne = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultOne.IsSuccess.Should().BeTrue();
|
resultOne.IsSuccess.Should().BeTrue();
|
||||||
resultOne.Value.Should().BeOfType<MessageResponse>();
|
resultOne.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -278,7 +307,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
request.Messages.Add(new(MessageRole.Assistant, resultOne.Value.Content));
|
||||||
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this paper?")]));
|
request.Messages.Add(new(MessageRole.User, [new TextContent("What is the main theme of this paper?")]));
|
||||||
|
|
||||||
var resultTwo = await client.CreateMessageAsync(request);
|
var resultTwo = await _client.CreateMessageAsync(request);
|
||||||
|
|
||||||
resultTwo.IsSuccess.Should().BeTrue();
|
resultTwo.IsSuccess.Should().BeTrue();
|
||||||
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
resultTwo.Value.Should().BeOfType<MessageResponse>();
|
||||||
@@ -395,6 +424,58 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
|
||||||
|
{
|
||||||
|
var fileName = "story.txt";
|
||||||
|
var fileType = "text/plain";
|
||||||
|
var filePath = TestFileHelper.GetTestFilePath("story.txt");
|
||||||
|
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||||
|
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
|
||||||
|
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
|
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||||
|
|
||||||
|
var request = new MessageRequest(
|
||||||
|
model: AnthropicModels.Claude35HaikuLatest,
|
||||||
|
messages: [
|
||||||
|
new(
|
||||||
|
MessageRole.User,
|
||||||
|
[
|
||||||
|
new DocumentContent(new FileSource(createdFile.Value.Id))
|
||||||
|
{
|
||||||
|
Title = "A Story",
|
||||||
|
Context = "This is a trustworthy document.",
|
||||||
|
Citations = new() { Enabled = true }
|
||||||
|
},
|
||||||
|
new TextContent("Can you tell me what the title of this story is?"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
|
||||||
|
var textContents = result.Value.Content.OfType<TextContent>();
|
||||||
|
|
||||||
|
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
|
||||||
|
{
|
||||||
|
sb.Append(content.Text);
|
||||||
|
return sb;
|
||||||
|
});
|
||||||
|
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
|
||||||
|
|
||||||
|
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
|
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||||
|
|
||||||
|
_filesToDelete.Add(createdFile.Value.Id);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse()
|
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForTextDocumentSource_ItShouldReturnCitationsInResponse()
|
||||||
{
|
{
|
||||||
@@ -516,6 +597,64 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
citations.OfType<ContentBlockLocationCitation>().Should().NotBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateMessageAsync_WhenStreamingAndCitationsAreEnabledForFileSource_ItShouldReturnCitationsInResponse()
|
||||||
|
{
|
||||||
|
var fileName = "story.txt";
|
||||||
|
var fileType = "text/plain";
|
||||||
|
var filePath = TestFileHelper.GetTestFilePath("story.txt");
|
||||||
|
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||||
|
var createFileRequest = new CreateFileRequest(fileContent, fileName, fileType);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
|
var createdFile = await client.CreateFileAsync(createFileRequest);
|
||||||
|
|
||||||
|
var request = new StreamMessageRequest(
|
||||||
|
model: AnthropicModels.Claude35HaikuLatest,
|
||||||
|
messages: [
|
||||||
|
new(
|
||||||
|
MessageRole.User,
|
||||||
|
[
|
||||||
|
new DocumentContent(new FileSource(createdFile.Value.Id))
|
||||||
|
{
|
||||||
|
Title = "A Story",
|
||||||
|
Context = "This is a trustworthy document.",
|
||||||
|
Citations = new() { Enabled = true }
|
||||||
|
},
|
||||||
|
new TextContent("Can you tell me what the title of this story is?"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = client.CreateMessageAsync(request);
|
||||||
|
|
||||||
|
var messageCompleteEvent = await result
|
||||||
|
.Where(e => e.Type is EventType.MessageComplete)
|
||||||
|
.FirstAsync();
|
||||||
|
|
||||||
|
var textContents = messageCompleteEvent.Data
|
||||||
|
.As<MessageCompleteEventData>()
|
||||||
|
.Message
|
||||||
|
.Content
|
||||||
|
.OfType<TextContent>();
|
||||||
|
|
||||||
|
var messageContent = textContents.Aggregate(new StringBuilder(), (sb, content) =>
|
||||||
|
{
|
||||||
|
sb.Append(content.Text);
|
||||||
|
return sb;
|
||||||
|
});
|
||||||
|
messageContent.ToString().Should().MatchRegex("The Forgotten Lighthouse");
|
||||||
|
|
||||||
|
var citations = textContents.SelectMany(static c => c.Citations is null ? [] : c.Citations);
|
||||||
|
citations.OfType<CharacterLocationCitation>().Should().NotBeEmpty();
|
||||||
|
|
||||||
|
_filesToDelete.Add(createdFile.Value.Id);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
|
public async Task CountMessageTokensAsync_WhenCalled_ItShouldReturnResponse()
|
||||||
{
|
{
|
||||||
@@ -697,7 +836,7 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
var fileContent = await File.ReadAllBytesAsync(filePath);
|
var fileContent = await File.ReadAllBytesAsync(filePath);
|
||||||
var request = new CreateFileRequest(fileContent, fileName, fileType);
|
var request = new CreateFileRequest(fileContent, fileName, fileType);
|
||||||
|
|
||||||
var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
var client = CreateClient(httpClient);
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
@@ -707,12 +846,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
result.Value.Should().BeOfType<AnthropicFile>();
|
result.Value.Should().BeOfType<AnthropicFile>();
|
||||||
result.Value.Name.Should().Be(fileName);
|
result.Value.Name.Should().Be(fileName);
|
||||||
result.Value.MimeType.Should().Be(fileType);
|
result.Value.MimeType.Should().Be(fileType);
|
||||||
|
|
||||||
|
_filesToDelete.Add(result.Value.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
|
public async Task ListFilesAsync_WhenCalled_ItShouldReturnPageOfFiles()
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
var client = CreateClient(httpClient);
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
@@ -725,12 +866,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
result.IsSuccess.Should().BeTrue();
|
result.IsSuccess.Should().BeTrue();
|
||||||
result.Value.Should().BeOfType<Page<AnthropicFile>>();
|
result.Value.Should().BeOfType<Page<AnthropicFile>>();
|
||||||
result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id);
|
result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id);
|
||||||
|
|
||||||
|
_filesToDelete.Add(createdFile.Value.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
|
public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles()
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
var client = CreateClient(httpClient);
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
@@ -741,14 +884,18 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
var responses = await client.ListAllFilesAsync(limit: 1).ToListAsync();
|
var responses = await client.ListAllFilesAsync(limit: 1).ToListAsync();
|
||||||
|
|
||||||
responses.Should().HaveCountGreaterThan(0);
|
responses.Should().HaveCountGreaterThan(0);
|
||||||
responses.Select(r => r.Value).SelectMany(p => p.Data)
|
responses.Select(r => r.Value)
|
||||||
.Should().ContainSingle(f => f.Id == createdFile.Value.Id);
|
.SelectMany(p => p.Data)
|
||||||
|
.Should()
|
||||||
|
.ContainSingle(f => f.Id == createdFile.Value.Id);
|
||||||
|
|
||||||
|
_filesToDelete.Add(createdFile.Value.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
|
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
var client = CreateClient(httpClient);
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
@@ -761,12 +908,14 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
result.IsSuccess.Should().BeTrue();
|
result.IsSuccess.Should().BeTrue();
|
||||||
result.Value.Should().BeOfType<AnthropicFile>();
|
result.Value.Should().BeOfType<AnthropicFile>();
|
||||||
result.Value.Id.Should().Be(createdFile.Value.Id);
|
result.Value.Id.Should().Be(createdFile.Value.Id);
|
||||||
|
|
||||||
|
_filesToDelete.Add(createdFile.Value.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse()
|
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse()
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
var client = CreateClient(httpClient);
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
@@ -780,4 +929,17 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
|
|||||||
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
|
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
|
||||||
result.Value.Id.Should().Be(createdFile.Value.Id);
|
result.Value.Id.Should().Be(createdFile.Value.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14");
|
||||||
|
var client = CreateClient(httpClient);
|
||||||
|
|
||||||
|
foreach (var file in _filesToDelete)
|
||||||
|
{
|
||||||
|
var result = await client.DeleteFileAsync(file);
|
||||||
|
result.IsSuccess.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class FileSourceTests : SerializationTest
|
||||||
|
{
|
||||||
|
private readonly string _testJson = @"{
|
||||||
|
""file_id"": ""id"",
|
||||||
|
""type"": ""file""
|
||||||
|
}";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
var result = new FileSource();
|
||||||
|
|
||||||
|
result.Type.Should().Be("file");
|
||||||
|
result.Id.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
var id = "id";
|
||||||
|
var type = "type";
|
||||||
|
|
||||||
|
var result = new FileSource()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Type = type,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.Id.Should().Be(id);
|
||||||
|
result.Type.Should().Be(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||||
|
{
|
||||||
|
var source = new FileSource() { Id = "id" };
|
||||||
|
|
||||||
|
var result = Serialize<Source>(source);
|
||||||
|
|
||||||
|
JsonAssert.Equal(_testJson, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenDeserialized_ItShouldHaveExpectedValues()
|
||||||
|
{
|
||||||
|
var result = Deserialize<Source>(_testJson);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new FileSource() { Id = "id" });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ public class ImageContentTests : SerializationTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_WhenCalledWithCacheControlAndMediatTypeIsNull_ItShouldThrowArgumentNullException()
|
public void Constructor_WhenCalledWithCacheControlAndMediaTypeIsNull_ItShouldThrowArgumentNullException()
|
||||||
{
|
{
|
||||||
var expectedData = "data";
|
var expectedData = "data";
|
||||||
var cacheControl = new EphemeralCacheControl();
|
var cacheControl = new EphemeralCacheControl();
|
||||||
@@ -98,6 +98,49 @@ public class ImageContentTests : SerializationTest
|
|||||||
action.Should().Throw<ArgumentNullException>();
|
action.Should().Throw<ArgumentNullException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithSource_ItShouldInitializeSource()
|
||||||
|
{
|
||||||
|
var source = new ImageSource("image/png", "data");
|
||||||
|
|
||||||
|
var result = new ImageContent(source);
|
||||||
|
|
||||||
|
result.Source.Should().BeSameAs(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithSourceAndCacheControl_ItShouldInitializeSourceAndCacheControl()
|
||||||
|
{
|
||||||
|
var source = new ImageSource("image/png", "data");
|
||||||
|
var cacheControl = new EphemeralCacheControl();
|
||||||
|
|
||||||
|
var result = new ImageContent(source, cacheControl);
|
||||||
|
|
||||||
|
result.Source.Should().BeSameAs(source);
|
||||||
|
result.CacheControl.Should().BeSameAs(cacheControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenSourceIsNull_ItShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
Source? source = null;
|
||||||
|
|
||||||
|
var action = () => new ImageContent(source!);
|
||||||
|
|
||||||
|
action.Should().Throw<ArgumentNullException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCacheControlIsNull_ItShouldThrowNullException()
|
||||||
|
{
|
||||||
|
var source = new ImageSource("image/png", "data");
|
||||||
|
CacheControl? cacheControl = null;
|
||||||
|
|
||||||
|
var action = () => new ImageContent(source, cacheControl!);
|
||||||
|
|
||||||
|
action.Should().Throw<ArgumentNullException>();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,4 +19,10 @@ public class SourceTypeTests
|
|||||||
{
|
{
|
||||||
SourceType.Text.Should().Be("text");
|
SourceType.Text.Should().Be("text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void File_WhenCalled_ItShouldReturnCorrectValue()
|
||||||
|
{
|
||||||
|
SourceType.File.Should().Be("file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace AnthropicClient.Tests.Unit.Models;
|
||||||
|
|
||||||
|
public class UrlSourceTests : SerializationTest
|
||||||
|
{
|
||||||
|
private readonly string _testJson = @"{
|
||||||
|
""type"": ""url"",
|
||||||
|
""url"": ""https://example.com/document.pdf""
|
||||||
|
}";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalled_ItShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
var result = new UrlSource();
|
||||||
|
|
||||||
|
result.Url.Should().BeEmpty();
|
||||||
|
result.Type.Should().Be("url");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithValues_ItShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
var url = "https://example.com/document.pdf";
|
||||||
|
var type = "type";
|
||||||
|
|
||||||
|
var result = new UrlSource()
|
||||||
|
{
|
||||||
|
Url = url,
|
||||||
|
Type = type,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.Url.Should().Be(url);
|
||||||
|
result.Type.Should().Be(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WhenCalledWithUrl_ItShouldInitializeProperties()
|
||||||
|
{
|
||||||
|
var url = "https://example.com/document.pdf";
|
||||||
|
|
||||||
|
var result = new UrlSource(url);
|
||||||
|
|
||||||
|
result.Url.Should().Be(url);
|
||||||
|
result.Type.Should().Be("url");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonSerialization_WhenSerialized_ItShouldHaveExpectedShape()
|
||||||
|
{
|
||||||
|
var url = "https://example.com/document.pdf";
|
||||||
|
var source = new UrlSource(url);
|
||||||
|
|
||||||
|
var result = Serialize<Source>(source);
|
||||||
|
|
||||||
|
JsonAssert.Equal(_testJson, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
|
||||||
|
{
|
||||||
|
var result = Deserialize<Source>(_testJson);
|
||||||
|
|
||||||
|
result.Should().BeEquivalentTo(new UrlSource("https://example.com/document.pdf"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user