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