diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 1cc6ebb..9d97391 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -34,7 +34,14 @@ public interface IAnthropicApiClient /// The message batch request to create. /// A task that represents the asynchronous operation. The task result contains the response as an where T is . Task> CreateMessageBatchAsync(MessageBatchRequest request); - + + /// + /// Gets a message batch asynchronously. + /// + /// The ID of the message batch to get. + /// A task that represents the asynchronous operation. The task result contains the response as an where T is . + Task> GetMessageBatchAsync(string batchId); + /// /// Counts the tokens in a message asynchronously. /// @@ -312,6 +319,23 @@ public class AnthropicApiClient : IAnthropicApiClient return AnthropicResult.Success(msgBatchResponse, anthropicHeaders); } + /// + public async Task> GetMessageBatchAsync(string batchId) + { + var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}"); + 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 msgBatchResponse = Deserialize(responseContent) ?? new MessageBatchResponse(); + return AnthropicResult.Success(msgBatchResponse, anthropicHeaders); + } + /// public async Task> CountMessageTokensAsync(CountMessageTokensRequest request) { diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index 2fff56f..4909936 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -361,4 +361,25 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf result.Value.Should().BeOfType(); result.Value.Id.Should().NotBeNullOrEmpty(); } + + [Fact] + public async Task GetMessageBatchAsync_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 getResult = await _client.GetMessageBatchAsync(createResult.Value.Id); + + getResult.IsSuccess.Should().BeTrue(); + getResult.Value.Should().BeOfType(); + getResult.Value.Id.Should().Be(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 ba2a6a2..d450495 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -1098,4 +1098,124 @@ public class AnthropicApiClientTests : IntegrationTest result.Value.Should().BeOfType(); result.Value.Should().BeEquivalentTo(new MessageBatchResponse()); } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalled_ItShouldReturnBatch() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""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"" + }" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + result.Value.Id.Should().Be("msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"); + result.Value.Type.Should().Be("message_batch"); + result.Value.ProcessingStatus.Should().Be("in_progress"); + result.Value.RequestCounts.Should().BeEquivalentTo(new MessageBatchRequestCounts + { + Processing = 100, + Succeeded = 50, + Errored = 30, + Canceled = 10, + Expired = 10 + }); + + result.Value.EndedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.CreatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ExpiresAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ArchivedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.CancelInitiatedAt.Should().Be(DateTimeOffset.Parse("2024-08-20T18:37:24.100435Z")); + result.Value.ResultsUrl.Should() + .Be("https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results"); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .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.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.BadRequest, + "application/json", + @"{}" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeOfType(); + result.Error.Error.Should().BeOfType(); + } + + [Fact] + public async Task GetMessageBatchAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse() + { + var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF"; + + _mockHttpMessageHandler + .WhenGetMessageBatchRequest(batchId) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{}" + ); + + var result = await Client.GetMessageBatchAsync(batchId); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().BeOfType(); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs index 1a9ef1c..b69a60d 100644 --- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs +++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs @@ -71,4 +71,10 @@ public static class MockHttpMessageHandlerExtensions return mockHttpMessageHandler .SetupBaseRequest(HttpMethod.Post, MessageBatchesEndpoint); } + + public static MockedRequest WhenGetMessageBatchRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId) + { + return mockHttpMessageHandler + .SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}"); + } } \ No newline at end of file