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

fix: address issues
This commit is contained in:
Stevan Freeborn
2024-07-19 22:39:51 -05:00
committed by GitHub
9 changed files with 160 additions and 1 deletions
@@ -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>
@@ -53,5 +54,11 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</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");
}
} }
File diff suppressed because one or more lines are too long
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");
}
}