From ca2ecdcfc79a5ff96c22eb564e7fc8be22e82a6f Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Sun, 12 Jan 2025 13:46:05 -0600
Subject: [PATCH] feat: implement ListMessageBatchesAsync method
---
src/AnthropicClient/AnthropicApiClient.cs | 26 ++
.../EndToEnd/AnthropicApiClientTests.cs | 24 +-
.../Integration/AnthropicApiClientTests.cs | 230 ++++++++++++++++++
.../Integration/IntegrationTest.cs | 6 +
4 files changed, 285 insertions(+), 1 deletion(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index add0174..bed8011 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -41,6 +41,13 @@ public interface IAnthropicApiClient
/// A task that represents the asynchronous operation. The task result contains the response as an where T is .
Task> GetMessageBatchAsync(string batchId);
+ ///
+ /// Lists the message batches asynchronously.
+ ///
+ /// The paging request to use for listing the message batches.
+ /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is .
+ Task>> ListMessageBatchesAsync(PagingRequest? request = null);
+
///
/// Gets the results of a message batch asynchronously.
///
@@ -342,6 +349,25 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult.Success(msgBatchResponse, anthropicHeaders);
}
+ ///
+ public async Task>> ListMessageBatchesAsync(PagingRequest? request = null)
+ {
+ var pagingRequest = request ?? new PagingRequest();
+ var endpoint = $"{MessageBatchesEndpoint}?{pagingRequest.ToQueryParameters()}";
+ var response = await SendRequestAsync(endpoint);
+ var anthropicHeaders = new AnthropicHeaders(response.Headers);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ var error = Deserialize(responseContent) ?? new AnthropicError();
+ return AnthropicResult>.Failure(error, anthropicHeaders);
+ }
+
+ var page = Deserialize>(responseContent) ?? new Page();
+ return AnthropicResult>.Success(page, anthropicHeaders);
+ }
+
///
public async Task>> GetMessageBatchResultsAsync(string batchId)
{
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index ad4c60b..5cb7170 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -2,7 +2,7 @@ using AnthropicClient.Tests.Files;
namespace AnthropicClient.Tests.EndToEnd;
-public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
+public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
{
[Fact]
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
@@ -381,4 +381,26 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
getResult.Value.Should().BeOfType();
getResult.Value.Id.Should().Be(createResult.Value.Id);
}
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalled_ItShouldReturnResponse()
+ {
+ var request = new MessageBatchRequest([
+ new(
+ Guid.NewGuid().ToString(),
+ new(
+ model: AnthropicModels.Claude3Haiku,
+ messages: [new(MessageRole.User, [new TextContent("Hello!")])]
+ )
+ ),
+ ]);
+
+ var createResult = await _client.CreateMessageBatchAsync(request);
+ var result = await _client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType>();
+ result.Value.Data.Should().HaveCountGreaterThan(0);
+ result.Value.Data.Should().ContainSingle(b => b.Id == createResult.Value.Id);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index e5a5a33..76d4bea 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1466,4 +1466,234 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledAndSuccessful_ItShouldReturnPageOfBatches()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .Respond(
+ HttpStatusCode.OK,
+ "application/json",
+ @"{
+ ""data"": [
+ {
+ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"",
+ ""type"": ""message_batch"",
+ ""processing_status"": ""in_progress"",
+ ""request_counts"": {
+ ""processing"": 100,
+ ""succeeded"": 50,
+ ""errored"": 30,
+ ""canceled"": 10,
+ ""expired"": 10
+ },
+ ""ended_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""created_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""expires_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""archived_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results""
+ }
+ ],
+ ""has_more"": true,
+ ""first_id"": ""1"",
+ ""last_id"": ""1""
+ }"
+ );
+
+ var result = await Client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType>();
+ result.Value.HasMore.Should().BeTrue();
+ result.Value.FirstId.Should().Be("1");
+ result.Value.LastId.Should().Be("1");
+ result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[]
+ {
+ new()
+ {
+ Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF",
+ Type = "message_batch",
+ ProcessingStatus = "in_progress",
+ RequestCounts = new MessageBatchRequestCounts
+ {
+ Processing = 100,
+ Succeeded = 50,
+ Errored = 30,
+ Canceled = 10,
+ Expired = 10
+ },
+ EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"
+ }
+ });
+ }
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledWithPagingRequestAndSuccessful_ItShouldReturnPageOfBatches()
+ {
+ var pagingRequest = new PagingRequest(afterId: "next_id", limit: 10);
+
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .WithQueryString(new Dictionary
+ {
+ { "after_id", pagingRequest.AfterId },
+ { "limit", pagingRequest.Limit.ToString() },
+ })
+ .Respond(
+ HttpStatusCode.OK,
+ "application/json",
+ @"{
+ ""data"": [
+ {
+ ""id"": ""msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"",
+ ""type"": ""message_batch"",
+ ""processing_status"": ""in_progress"",
+ ""request_counts"": {
+ ""processing"": 100,
+ ""succeeded"": 50,
+ ""errored"": 30,
+ ""canceled"": 10,
+ ""expired"": 10
+ },
+ ""ended_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""created_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""expires_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""archived_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""cancel_initiated_at"": ""2024-08-20T18:37:24.100435Z"",
+ ""results_url"": ""https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results""
+ }
+ ],
+ ""has_more"": true,
+ ""first_id"": ""1"",
+ ""last_id"": ""1""
+ }"
+ );
+
+ var result = await Client.ListMessageBatchesAsync(pagingRequest);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType>();
+ result.Value.HasMore.Should().BeTrue();
+ result.Value.FirstId.Should().Be("1");
+ result.Value.LastId.Should().Be("1");
+ result.Value.Data.Should().BeEquivalentTo(new MessageBatchResponse[]
+ {
+ new()
+ {
+ Id = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF",
+ Type = "message_batch",
+ ProcessingStatus = "in_progress",
+ RequestCounts = new MessageBatchRequestCounts
+ {
+ Processing = 100,
+ Succeeded = 50,
+ Errored = 30,
+ Canceled = 10,
+ Expired = 10
+ },
+ EndedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ CreatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ExpiresAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ArchivedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ CancelInitiatedAt = DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z"),
+ ResultsUrl = "https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"
+ }
+ });
+ }
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledAndNoBatchesReturned_ItShouldReturnEmptyList()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .Respond(
+ HttpStatusCode.OK,
+ "application/json",
+ @"{
+ ""data"": [],
+ ""has_more"": false,
+ ""first_id"": null,
+ ""last_id"": null
+ }"
+ );
+
+ var result = await Client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType>();
+ result.Value.HasMore.Should().BeFalse();
+ result.Value.FirstId.Should().BeNull();
+ result.Value.LastId.Should().BeNull();
+ result.Value.Data.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledAndErrorReturned_ItShouldHandleError()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .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 result = await Client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeFalse();
+ result.Error.Should().BeOfType();
+ result.Error.Error.Should().BeOfType();
+ }
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledRequestFailsAndCanNotDeserializeError_ItShouldReturnUnknownError()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .Respond(
+ HttpStatusCode.BadRequest,
+ "application/json",
+ @"null"
+ );
+
+ var result = await Client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeFalse();
+ result.Error.Should().BeOfType();
+ result.Error.Error.Should().BeOfType();
+ }
+
+ [Fact]
+ public async Task ListMessageBatchesAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyPage()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .Respond(
+ HttpStatusCode.OK,
+ "application/json",
+ @"null"
+ );
+
+ var result = await Client.ListMessageBatchesAsync();
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeOfType>();
+ result.Value.HasMore.Should().BeFalse();
+ result.Value.FirstId.Should().BeEmpty();
+ result.Value.LastId.Should().BeEmpty();
+ result.Value.Data.Should().BeEmpty();
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index 90653d4..465319d 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -85,4 +85,10 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}/results");
}
+
+ public static MockedRequest WhenListMessageBatchesRequest(this MockHttpMessageHandler mockHttpMessageHandler)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Get, MessageBatchesEndpoint);
+ }
}
\ No newline at end of file