feat: add ListAllMessageBatchesAsync method and corresponding tests

This commit is contained in:
Stevan Freeborn
2025-01-13 12:42:23 -06:00
parent ca2ecdcfc7
commit 180a090195
3 changed files with 213 additions and 30 deletions
+53 -29
View File
@@ -48,6 +48,13 @@ public interface IAnthropicApiClient
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
Task<AnthropicResult<Page<MessageBatchResponse>>> ListMessageBatchesAsync(PagingRequest? request = null);
/// <summary>
/// Lists all message batches asynchronously.
/// </summary>
/// <param name="limit">The maximum number of message batches to return in each page.</param>
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="MessageBatchResponse"/>.</returns>
IAsyncEnumerable<AnthropicResult<Page<MessageBatchResponse>>> ListAllMessageBatchesAsync(int limit = 20);
/// <summary>
/// Gets the results of a message batch asynchronously.
/// </summary>
@@ -368,6 +375,15 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult<Page<MessageBatchResponse>>.Success(page, anthropicHeaders);
}
/// <inheritdoc/>
public async IAsyncEnumerable<AnthropicResult<Page<MessageBatchResponse>>> ListAllMessageBatchesAsync(int limit = 20)
{
await foreach (var result in GetAllPagesAsync<MessageBatchResponse>(MessageBatchesEndpoint, limit))
{
yield return result;
}
}
/// <inheritdoc/>
public async Task<AnthropicResult<IAsyncEnumerable<MessageBatchResultItem>>> GetMessageBatchResultsAsync(string batchId)
{
@@ -439,37 +455,10 @@ public class AnthropicApiClient : IAnthropicApiClient
/// <inheritdoc/>
public async IAsyncEnumerable<AnthropicResult<Page<AnthropicModel>>> ListAllModelsAsync(int limit = 20)
{
var pagingRequest = new PagingRequest(limit: limit);
string Endpoint() => $"{ModelsEndpoint}?{pagingRequest.ToQueryParameters()}";
bool hasMore;
do
await foreach (var result in GetAllPagesAsync<AnthropicModel>(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<AnthropicError>(responseContent) ?? new AnthropicError();
yield return AnthropicResult<Page<AnthropicModel>>.Failure(error, anthropicHeaders);
yield break;
yield return result;
}
var page = Deserialize<Page<AnthropicModel>>(responseContent) ?? new Page<AnthropicModel>();
if (page.HasMore && page.LastId is not null)
{
hasMore = true;
pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
}
else
{
hasMore = false;
}
yield return AnthropicResult<Page<AnthropicModel>>.Success(page, anthropicHeaders);
} while (hasMore);
}
/// <inheritdoc/>
@@ -490,6 +479,41 @@ public class AnthropicApiClient : IAnthropicApiClient
return AnthropicResult<AnthropicModel>.Success(model, anthropicHeaders);
}
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(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<AnthropicError>(responseContent) ?? new AnthropicError();
yield return AnthropicResult<Page<T>>.Failure(error, anthropicHeaders);
yield break;
}
var page = Deserialize<Page<T>>(responseContent) ?? new Page<T>();
if (page.HasMore && page.LastId is not null)
{
hasMore = true;
pagingRequest = new PagingRequest(limit: limit, afterId: page.LastId);
}
else
{
hasMore = false;
}
yield return AnthropicResult<Page<T>>.Success(page, anthropicHeaders);
} while (hasMore);
}
private ToolCall? GetToolCall(MessageResponse response, List<Tool> tools)
{
var toolUse = response.Content.OfType<ToolUseContent>().FirstOrDefault();
@@ -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);
}
}
@@ -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<string, string>()
{
{ "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<string, string>()
{
{ "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<Page<MessageBatchResponse>>();
await foreach (var response in pageResponses)
{
response.IsSuccess.Should().BeTrue();
response.Value.Should().BeOfType<Page<MessageBatchResponse>>();
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<Page<MessageBatchResponse>>()
{
new()
{
Data = [expectedMessageBatchResponse],
FirstId = "1",
LastId = "1",
HasMore = true
},
new()
{
Data = [expectedMessageBatchResponse],
FirstId = "2",
LastId = "2",
HasMore = false
}
});
}
}