Merge pull request #11 from StevanFreeborn/stevanfreeborn/fix/missing-type-property-and-response-handling

fix: address issues
This commit is contained in:
Stevan Freeborn authored and GitHub committed 2024-07-19 22:39:51 -05:00
commit d27538b656
9 files changed
+160 -1

No files matched your search

@@ -95,6 +95,14 @@ public class AnthropicApiClient : IAnthropicApiClient
public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request) public async IAsyncEnumerable<AnthropicEvent> CreateMessageAsync(StreamMessageRequest request)
{ {
var response = await SendRequestAsync(request); var response = await SendRequestAsync(request);
if (response.IsSuccessStatusCode is false)
{
var error = Deserialize<AnthropicError>(await response.Content.ReadAsStringAsync()) ?? new AnthropicError();
yield return new AnthropicEvent(EventType.Error, new ErrorEventData(error.Error));
yield break;
}
var anthropicHeaders = new AnthropicHeaders(response.Headers); var anthropicHeaders = new AnthropicHeaders(response.Headers);
using var responseContent = await response.Content.ReadAsStreamAsync(); using var responseContent = await response.Content.ReadAsStreamAsync();
@@ -20,6 +20,11 @@ public class ImageSource
/// </summary> /// </summary>
public string Data { get; init; } = string.Empty; public string Data { get; init; } = string.Empty;
/// <summary>
/// Gets the type of encoding of the image data.
/// </summary>
public string Type { get; init; } = "base64";
[JsonConstructor] [JsonConstructor]
internal ImageSource() internal ImageSource()
{ {
@@ -6,6 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);IDE0039</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -54,4 +55,10 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="Files/**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>
@@ -1,3 +1,6 @@
using Xunit.Abstractions;
using Xunit.Sdk;
namespace AnthropicClient.Tests.EndToEnd; namespace AnthropicClient.Tests.EndToEnd;
public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture) public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
@@ -56,4 +59,40 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
} }
} }
} }
[Fact]
public async Task CreateMessageAsync_WhenImageIsSent_ItShouldReturnResponse()
{
var imagePath = Path.Combine(Directory.GetCurrentDirectory(), "Files", "base64-elephant.txt");
var mediaType = "image/jpeg";
var base64Data = await File.ReadAllTextAsync(imagePath);
var request = new MessageRequest(
model: AnthropicModels.Claude3Haiku,
messages: [
new(MessageRole.User, [
new ImageContent(mediaType, base64Data),
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("", (acc, content) =>
{
if (content is TextContent textContent)
{
acc += textContent.Text;
}
return acc;
});
text.Should().Contain("elephant");
}
} }
@@ -0,0 +1 @@
/9j/4AAQSkZJRgABAQEBLAEsAAD/4QBWRXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAAAAAEsAAAAAQAAASwAAAAB/+0ALFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAPHAFaAAMbJUccAQAAAgAEAP/hDIFodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0n77u/JyBpZD0nVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkJz8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0nYWRvYmU6bnM6bWV0YS8nIHg6eG1wdGs9J0ltYWdlOjpFeGlmVG9vbCAxMC4xMCc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczp0aWZmPSdodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyc+CiAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICA8dGlmZjpYUmVzb2x1dGlvbj4zMDAvMTwvdGlmZjpYUmVzb2x1dGlvbj4KICA8dGlmZjpZUmVzb2x1dGlvbj4zMDAvMTwvdGlmZjpZUmVzb2x1dGlvbj4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PScnCiAgeG1sbnM6eG1wTU09J2h0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8nPgogIDx4bXBNTTpEb2N1bWVudElEPmFkb2JlOmRvY2lkOnN0b2NrOmQ1MTJjMzg3LWVkMGEtNDEwYy04MmU5LTM1NTMwZTFlMDRjODwveG1wTU06RG9jdW1lbnRJRD4KICA8eG1wTU06SW5zdGFuY2VJRD54bXAuaWlkOmQ2ZDI3ZjdiLTZmZDItNDdiYi1iODhhLTE1NWFhMjRjZTZiNjwveG1wTU06SW5zdGFuY2VJRD4KIDwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAo8P3hwYWNrZXQgZW5kPSd3Jz8+/9sAQwAFAwQEBAMFBAQEBQUFBgcMCAcHBwcPCwsJDBEPEhIRDxERExYcFxMUGhURERghGBodHR8fHxMXIiQiHiQcHh8e/9sAQwEFBQUHBgcOCAgOHhQRFB4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e/8AAEQgBaAH0AwERAAIRAQMRAf/EAB0AAQABBQEBAQAAAAAAAAAAAAAEAwUGBwgCAQn/xABIEAACAQMDAQUFBQYDCAEEAQUBAgMABBEFEiExBhNBUWEHInGBkQgUMqGxI0JSwdHwFTNiFiRygpKi4fFDCSVTwrIXJzRjc//EABgBAQEBAQEAAAAAAAAAAAAAAAABAgME/8QAKBEBAQEBAAICAgEEAQUAAAAAAAERAiExEkEDURMiMmFxBBRCUoHR/9oADAMBAAIRAxEAPwDsugUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgUCgULine truncated
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -321,4 +321,42 @@ public class AnthropicApiClientTests : IntegrationTest
toolCallResult.IsSuccess.Should().BeTrue(); toolCallResult.IsSuccess.Should().BeTrue();
toolCallResult.Value.Should().Be(getWeather("San Francisco, CA", "fahrenheit")); toolCallResult.Value.Should().Be(getWeather("San Francisco, CA", "fahrenheit"));
} }
[Fact]
public async Task CreateMessageAsync_WhenCalledMessageIsStreamAndRequestFails_ItShouldReturnErrorEvent()
{
_mockHttpMessageHandler
.WhenCreateStreamMessageRequest()
.Respond(
HttpStatusCode.BadRequest,
"application/json",
@"{
""type"": ""error"",
""error"": {
""type"": ""invalid_request_error"",
""message"": ""messages: roles must alternate between user and assistant, but found multiple user roles in a row""
}
}"
);
var request = new StreamMessageRequest(
model: AnthropicModels.Claude35Sonnet,
messages: [
new(MessageRole.User, [new TextContent("Hello!")]),
new(MessageRole.User, [new TextContent("Hello!")])
]
);
var result = Client.CreateMessageAsync(request);
var events = await result.ToListAsync();
events.Should().HaveCount(1);
events[0].Type.Should().Be(EventType.Error);
events[0].Data.Should().BeOfType<ErrorEventData>();
events[0].Data.Should().BeEquivalentTo(new ErrorEventData(
new InvalidRequestError(
"messages: roles must alternate between user and assistant, but found multiple user roles in a row"
)
));
}
} }
@@ -5,7 +5,8 @@ public class ImageContentTests : SerializationTest
private readonly string _testJson = @"{ private readonly string _testJson = @"{
""source"": { ""source"": {
""media_type"": ""image/png"", ""media_type"": ""image/png"",
""data"": ""data"" ""data"": ""data"",
""type"": ""base64""
}, },
""type"": ""image"" ""type"": ""image""
}"; }";
@@ -0,0 +1,60 @@
namespace AnthropicClient.Tests.Unit.Models;
public class ImageSourceTests
{
[Fact]
public void Constructor_WhenCalledWithValidArguments_ItShouldSetProperties()
{
var mediaType = "image/jpeg";
var data = "base64data";
var imageSource = new ImageSource(mediaType, data);
imageSource.MediaType.Should().Be(mediaType);
imageSource.Data.Should().Be(data);
}
[Fact]
public void Constructor_WhenCalledWithInvalidMediaType_ItShouldThrowArgumentException()
{
var mediaType = "invalid";
var data = "base64data";
var action = () => new ImageSource(mediaType, data);
action.Should().Throw<ArgumentException>();
}
[Fact]
public void Constructor_WhenCalledWithNullMediaType_ItShouldThrowArgumentNullException()
{
string? mediaType = null;
var data = "base64data";
var action = () => new ImageSource(mediaType!, data);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalledWithNullData_ItShouldThrowArgumentNullException()
{
var mediaType = "image/jpeg";
string? data = null;
var action = () => new ImageSource(mediaType, data!);
action.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Constructor_WhenCalled_ItShouldHaveTypePropertySetToBase64()
{
var mediaType = "image/jpeg";
var data = "base64data";
var imageSource = new ImageSource(mediaType, data);
imageSource.Type.Should().Be("base64");
}
}