From 180a0901959b88b27981f063b0be7a267a141802 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Mon, 13 Jan 2025 12:42:23 -0600
Subject: [PATCH] feat: add ListAllMessageBatchesAsync method and corresponding
tests
---
src/AnthropicClient/AnthropicApiClient.cs | 84 +++++++-----
.../EndToEnd/AnthropicApiClientTests.cs | 32 +++++
.../Integration/AnthropicApiClientTests.cs | 127 ++++++++++++++++++
3 files changed, 213 insertions(+), 30 deletions(-)
diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index bed8011..8c0b3fb 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -48,6 +48,13 @@ public interface IAnthropicApiClient
/// 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);
+ ///
+ /// Lists all message batches asynchronously.
+ ///
+ /// The maximum number of message batches to return in each page.
+ /// An asynchronous enumerable that yields the response as an where T is where T is .
+ IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20);
+
///
/// Gets the results of a message batch asynchronously.
///
@@ -368,6 +375,15 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult>.Success(page, anthropicHeaders);
}
+ ///
+ public async IAsyncEnumerable>> ListAllMessageBatchesAsync(int limit = 20)
+ {
+ await foreach (var result in GetAllPagesAsync(MessageBatchesEndpoint, limit))
+ {
+ yield return result;
+ }
+ }
+
///
public async Task>> GetMessageBatchResultsAsync(string batchId)
{
@@ -439,37 +455,10 @@ public class AnthropicApiClient : IAnthropicApiClient
///
public async IAsyncEnumerable>> ListAllModelsAsync(int limit = 20)
{
- var pagingRequest = new PagingRequest(limit: limit);
- string Endpoint() => $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
- bool hasMore;
-
- do
+ await foreach (var result in GetAllPagesAsync(ModelsEndpoint, limit))
{
- 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();
- yield return AnthropicResult>.Failure(error, anthropicHeaders);
- yield break;
- }
-
- var page = Deserialize>(responseContent) ?? new Page();
-
- if (page.HasMore && page.LastId is not null)
- {
- hasMore = true;
- pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
- }
- else
- {
- hasMore = false;
- }
-
- yield return AnthropicResult>.Success(page, anthropicHeaders);
- } while (hasMore);
+ yield return result;
+ }
}
///
@@ -490,6 +479,41 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult.Success(model, anthropicHeaders);
}
+ private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20)
+ {
+ var pagingRequest = new PagingRequest(limit: limit);
+ string Endpoint() => $"{endpoint}?{pagingRequest.ToQueryParameters()}";
+ bool hasMore;
+
+ do
+ {
+ 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();
+ yield return AnthropicResult>.Failure(error, anthropicHeaders);
+ yield break;
+ }
+
+ var page = Deserialize>(responseContent) ?? new Page();
+
+ if (page.HasMore && page.LastId is not null)
+ {
+ hasMore = true;
+ pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
+ }
+ else
+ {
+ hasMore = false;
+ }
+
+ yield return AnthropicResult>.Success(page, anthropicHeaders);
+ } while (hasMore);
+ }
+
private ToolCall? GetToolCall(MessageResponse response, List tools)
{
var toolUse = response.Content.OfType().FirstOrDefault();
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 5cb7170..4cdd660 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -403,4 +403,36 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
result.Value.Data.Should().HaveCountGreaterThan(0);
result.Value.Data.Should().ContainSingle(b => b.Id == createResult.Value.Id);
}
+
+ [Fact]
+ public async Task ListAllMessageBatchesAsync_WhenCalled_ItShouldReturnResponse()
+ {
+ var createRequest = (string id) => new MessageBatchRequest([
+ new(
+ id,
+ new(
+ model: AnthropicModels.Claude3Haiku,
+ messages: [new(MessageRole.User, [new TextContent("Hello!")])]
+ )
+ ),
+ ]);
+
+ var requestNumberOne = createRequest(Guid.NewGuid().ToString());
+ var requestNumberTwo = createRequest(Guid.NewGuid().ToString());
+
+ var createResultOne = await _client.CreateMessageBatchAsync(requestNumberOne);
+ var createResultTwo = await _client.CreateMessageBatchAsync(requestNumberTwo);
+
+ var responses = await _client.ListAllMessageBatchesAsync(limit: 1).ToListAsync();
+
+ responses.Should().HaveCountGreaterThan(2);
+
+ var batches = responses
+ .Where(r => r.IsSuccess)
+ .Select(r => r.Value)
+ .SelectMany(r => r.Data);
+
+ batches.Should().ContainSingle(b => b.Id == createResultOne.Value.Id);
+ batches.Should().ContainSingle(b => b.Id == createResultTwo.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 76d4bea..2b458c2 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1696,4 +1696,131 @@ public class AnthropicApiClientTests : IntegrationTest
result.Value.LastId.Should().BeEmpty();
result.Value.Data.Should().BeEmpty();
}
+
+ [Fact]
+ public async Task ListAllMessageBatchesAsync_WhenCalled_ItShouldReturnAllBatches()
+ {
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .WithExactQueryString(new Dictionary()
+ {
+ { "limit", "20" },
+ })
+ .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""
+ }"
+ );
+
+ _mockHttpMessageHandler
+ .WhenListMessageBatchesRequest()
+ .WithExactQueryString(new Dictionary()
+ {
+ { "after_id", "1" },
+ { "limit", "20" },
+ })
+ .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"": false,
+ ""first_id"": ""2"",
+ ""last_id"": ""2""
+ }"
+ );
+
+ var pageResponses = Client.ListAllMessageBatchesAsync();
+ var collectedPages = new List>();
+
+ await foreach (var response in pageResponses)
+ {
+ response.IsSuccess.Should().BeTrue();
+ response.Value.Should().BeOfType>();
+ collectedPages.Add(response.Value);
+ }
+
+ var expectedMessageBatchResponse = new MessageBatchResponse()
+ {
+ 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"
+ };
+
+ collectedPages.Should().HaveCount(2);
+ collectedPages.Should().BeEquivalentTo(new List>()
+ {
+ new()
+ {
+ Data = [expectedMessageBatchResponse],
+ FirstId = "1",
+ LastId = "1",
+ HasMore = true
+ },
+ new()
+ {
+ Data = [expectedMessageBatchResponse],
+ FirstId = "2",
+ LastId = "2",
+ HasMore = false
+ }
+ });
+ }
}
\ No newline at end of file