diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs
index 9d97391..cb5f0c5 100644
--- a/src/AnthropicClient/AnthropicApiClient.cs
+++ b/src/AnthropicClient/AnthropicApiClient.cs
@@ -34,14 +34,21 @@ 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);
-
+
+ ///
+ /// Gets the results of a message batch asynchronously.
+ ///
+ /// The ID of the message batch to get the results for.
+ /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is .
+ Task>> GetMessageBatchResultsAsync(string batchId);
+
///
/// Counts the tokens in a message asynchronously.
///
@@ -325,17 +332,49 @@ public class AnthropicApiClient : IAnthropicApiClient
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>> GetMessageBatchResultsAsync(string batchId)
+ {
+ var response = await SendRequestAsync($"{MessageBatchesEndpoint}/{batchId}/results");
+ var anthropicHeaders = new AnthropicHeaders(response.Headers);
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ var content = await response.Content.ReadAsStringAsync();
+ var error = Deserialize(content) ?? new AnthropicError();
+ return AnthropicResult>.Failure(error, anthropicHeaders);
+ }
+
+ async IAsyncEnumerable ReadResults()
+ {
+ using var responseContent = await response.Content.ReadAsStreamAsync();
+ using var streamReader = new StreamReader(responseContent);
+
+ var line = await streamReader.ReadLineAsync();
+
+ while (line is not null)
+ {
+ var resultItem = Deserialize(line) ?? new MessageBatchResultItem();
+ yield return resultItem;
+
+ line = await streamReader.ReadLineAsync();
+ }
+ }
+
+ return AnthropicResult>.Success(ReadResults(), anthropicHeaders);
+ }
+
///
public async Task> CountMessageTokensAsync(CountMessageTokensRequest request)
{
diff --git a/src/AnthropicClient/Json/JsonSerializationOptions.cs b/src/AnthropicClient/Json/JsonSerializationOptions.cs
index 6e9dece..c1d5c40 100644
--- a/src/AnthropicClient/Json/JsonSerializationOptions.cs
+++ b/src/AnthropicClient/Json/JsonSerializationOptions.cs
@@ -17,6 +17,7 @@ static class JsonSerializationOptions
new EventDataConverter(),
new ContentDeltaConverter(),
new JsonStringEnumConverter(),
+ new MessageBatchResultConverter(),
},
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
diff --git a/src/AnthropicClient/Json/MessageBatchResultConverter.cs b/src/AnthropicClient/Json/MessageBatchResultConverter.cs
new file mode 100644
index 0000000..1df20b2
--- /dev/null
+++ b/src/AnthropicClient/Json/MessageBatchResultConverter.cs
@@ -0,0 +1,29 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using AnthropicClient.Models;
+
+namespace AnthropicClient.Json;
+
+class MessageBatchResultConverter : JsonConverter
+{
+ public override MessageBatchResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var jsonDocument = JsonDocument.ParseValue(ref reader);
+ var root = jsonDocument.RootElement;
+ var type = root.GetProperty("type").GetString();
+ return type switch
+ {
+ MessageBatchResultType.Succeeded => JsonSerializer.Deserialize(root.GetRawText(), options)!,
+ MessageBatchResultType.Errored => JsonSerializer.Deserialize(root.GetRawText(), options)!,
+ MessageBatchResultType.Canceled => JsonSerializer.Deserialize(root.GetRawText(), options)!,
+ MessageBatchResultType.Expired => JsonSerializer.Deserialize(root.GetRawText(), options)!,
+ _ => throw new JsonException($"Unknown message batch result type: {type}")
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, MessageBatchResult value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value, value.GetType(), options);
+ }
+}
\ No newline at end of file
diff --git a/src/AnthropicClient/Models/CanceledMessageBatchResult.cs b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs
new file mode 100644
index 0000000..878bff6
--- /dev/null
+++ b/src/AnthropicClient/Models/CanceledMessageBatchResult.cs
@@ -0,0 +1,14 @@
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result that was cancelled.
+///
+public class CanceledMessageBatchResult : MessageBatchResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CanceledMessageBatchResult() : base(MessageBatchResultType.Canceled)
+ {
+ }
+}
diff --git a/src/AnthropicClient/Models/ErroredMessageBatchResult.cs b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs
new file mode 100644
index 0000000..4e7dbf5
--- /dev/null
+++ b/src/AnthropicClient/Models/ErroredMessageBatchResult.cs
@@ -0,0 +1,19 @@
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result that contains an error response.
+///
+public class ErroredMessageBatchResult : MessageBatchResult
+{
+ ///
+ /// Gets the error of the message batch result.
+ ///
+ public AnthropicError Error { get; init; } = new AnthropicError();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ErroredMessageBatchResult() : base(MessageBatchResultType.Errored)
+ {
+ }
+}
diff --git a/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs b/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs
new file mode 100644
index 0000000..11f9f97
--- /dev/null
+++ b/src/AnthropicClient/Models/ExpiredMessageBatchResult.cs
@@ -0,0 +1,14 @@
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result that has expired.
+///
+public class ExpiredMessageBatchResult : MessageBatchResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ExpiredMessageBatchResult() : base(MessageBatchResultType.Expired)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/AnthropicClient/Models/MessageBatchResult.cs b/src/AnthropicClient/Models/MessageBatchResult.cs
new file mode 100644
index 0000000..6531475
--- /dev/null
+++ b/src/AnthropicClient/Models/MessageBatchResult.cs
@@ -0,0 +1,26 @@
+using AnthropicClient.Utils;
+
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result.
+///
+public abstract class MessageBatchResult
+{
+ ///
+ /// Gets the type of the message batch result.
+ ///
+ public string Type { get; init; } = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the message batch result.
+ /// An instance of the class.
+ public MessageBatchResult(string type)
+ {
+ ArgumentValidator.ThrowIfNullOrWhitespace(type, nameof(type));
+
+ Type = type;
+ }
+}
\ No newline at end of file
diff --git a/src/AnthropicClient/Models/MessageBatchResultItem.cs b/src/AnthropicClient/Models/MessageBatchResultItem.cs
new file mode 100644
index 0000000..14f9be6
--- /dev/null
+++ b/src/AnthropicClient/Models/MessageBatchResultItem.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result item.
+///
+public class MessageBatchResultItem
+{
+ ///
+ /// Gets the custom ID of the message batch result item.
+ ///
+ [JsonPropertyName("custom_id")]
+ public string CustomId { get; init; } = string.Empty;
+
+ ///
+ /// Gets the result of the message batch result item.
+ ///
+ public MessageBatchResult Result { get; init; } = default!;
+}
diff --git a/src/AnthropicClient/Models/MessageBatchResultType.cs b/src/AnthropicClient/Models/MessageBatchResultType.cs
new file mode 100644
index 0000000..187c2c2
--- /dev/null
+++ b/src/AnthropicClient/Models/MessageBatchResultType.cs
@@ -0,0 +1,27 @@
+namespace AnthropicClient.Models;
+
+///
+/// Represents the types of message batch results.
+///
+public static class MessageBatchResultType
+{
+ ///
+ /// Represents a succeeded message batch result.
+ ///
+ public const string Succeeded = "succeeded";
+
+ ///
+ /// Represents an errored message batch result.
+ ///
+ public const string Errored = "errored";
+
+ ///
+ /// Represents a canceled message batch result.
+ ///
+ public const string Canceled = "canceled";
+
+ ///
+ /// Represents an expired message batch result.
+ ///
+ public const string Expired = "expired";
+}
diff --git a/src/AnthropicClient/Models/SucceededMessageBatchResult.cs b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
new file mode 100644
index 0000000..82260e8
--- /dev/null
+++ b/src/AnthropicClient/Models/SucceededMessageBatchResult.cs
@@ -0,0 +1,20 @@
+namespace AnthropicClient.Models;
+
+///
+/// Represents a message batch result that contains a message response.
+///
+public class SucceededMessageBatchResult : MessageBatchResult
+{
+ ///
+ /// Gets the message of the message batch result.
+ ///
+ public MessageResponse Message { get; init; } = new MessageResponse();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An instance of the class.
+ public SucceededMessageBatchResult() : base(MessageBatchResultType.Succeeded)
+ {
+ }
+}
diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
index 4909936..ad4c60b 100644
--- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs
@@ -1,10 +1,9 @@
+using AnthropicClient.Tests.Files;
+
namespace AnthropicClient.Tests.EndToEnd;
public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(configFixture)
{
- private string GetTestFilePath(string fileName) =>
- Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName);
-
[Fact]
public async Task CreateMessageAsync_WhenCalled_ItShouldReturnResponse()
{
@@ -63,7 +62,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
[Fact]
public async Task CreateMessageAsync_WhenImageIsSent_ItShouldReturnResponse()
{
- var imagePath = GetTestFilePath("elephant.jpg");
+ var imagePath = TestFileHelper.GetTestFilePath("elephant.jpg");
var mediaType = "image/jpeg";
var bytes = await File.ReadAllBytesAsync(imagePath);
var base64Data = Convert.ToBase64String(bytes);
@@ -102,7 +101,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
{
var client = CreateClient(new HttpClient());
- var storyPath = GetTestFilePath("story.txt");
+ var storyPath = TestFileHelper.GetTestFilePath("story.txt");
var storyText = await File.ReadAllTextAsync(storyPath);
var request = new MessageRequest(
@@ -141,7 +140,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
{
var client = CreateClient(new HttpClient());
- var storyPath = GetTestFilePath("story.txt");
+ var storyPath = TestFileHelper.GetTestFilePath("story.txt");
var storyText = await File.ReadAllTextAsync(storyPath);
var request = new MessageRequest(
@@ -217,7 +216,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
[Fact]
public async Task CreateMessageAsync_WhenProvidedWithPDF_ItShouldReturnResponse()
{
- var pdfPath = GetTestFilePath("addendum.pdf");
+ var pdfPath = TestFileHelper.GetTestFilePath("addendum.pdf");
var bytes = await File.ReadAllBytesAsync(pdfPath);
var base64Data = Convert.ToBase64String(bytes);
@@ -253,7 +252,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
[Fact]
public async Task CreateMessageAsync_WhenProvidedWithPDFWithCacheControl_ItShouldUseCache()
{
- var pdfPath = GetTestFilePath("addendum.pdf");
+ var pdfPath = TestFileHelper.GetTestFilePath("addendum.pdf");
var bytes = await File.ReadAllBytesAsync(pdfPath);
var base64Data = Convert.ToBase64String(bytes);
@@ -354,9 +353,9 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
)
),
]);
-
+
var result = await _client.CreateMessageBatchAsync(request);
-
+
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType();
result.Value.Id.Should().NotBeNullOrEmpty();
@@ -374,7 +373,7 @@ public class ClientTests(ConfigurationFixture configFixture) : EndToEndTest(conf
)
),
]);
-
+
var createResult = await _client.CreateMessageBatchAsync(request);
var getResult = await _client.GetMessageBatchAsync(createResult.Value.Id);
diff --git a/tests/AnthropicClient.Tests/Files/TestFileHelper.cs b/tests/AnthropicClient.Tests/Files/TestFileHelper.cs
new file mode 100644
index 0000000..efc9ca1
--- /dev/null
+++ b/tests/AnthropicClient.Tests/Files/TestFileHelper.cs
@@ -0,0 +1,7 @@
+namespace AnthropicClient.Tests.Files;
+
+static class TestFileHelper
+{
+ public static string GetTestFilePath(string fileName) =>
+ Path.Combine(Directory.GetCurrentDirectory(), "Files", fileName);
+}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Files/batch_results.jsonl b/tests/AnthropicClient.Tests/Files/batch_results.jsonl
new file mode 100644
index 0000000..3647eca
--- /dev/null
+++ b/tests/AnthropicClient.Tests/Files/batch_results.jsonl
@@ -0,0 +1,5 @@
+{"custom_id":"my-fifth-request","result":{"type":"errored","error":{"type":"error","error":{"type":"not_found_error","message":"The requested resource could not be found."}}}}
+{"custom_id":"my-fourth-request","result":{"type":"expired"}}
+{"custom_id":"my-third-request","result":{"type":"canceled"}}
+{"custom_id":"my-second-request","result":{"type":"succeeded","message":{"id":"msg_014VwiXbi91y3JMjcpyGBHX5","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Hello again! It's nice to see you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":11,"output_tokens":36}}}}
+{"custom_id":"my-first-request","result":{"type":"succeeded","message":{"id":"msg_01FqfsLoHwgeFbguDgpz48m7","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Hello! How can I assist you today? Feel free to ask me any questions or let me know if there's anything you'd like to chat about."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":34}}}}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
index d450495..f3fa91e 100644
--- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
+++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs
@@ -1,3 +1,6 @@
+using AnthropicClient.Tests.Files;
+using AnthropicClient.Tests.Unit;
+
namespace AnthropicClient.Tests.Integration;
public class AnthropicApiClientTests : IntegrationTest
@@ -1032,7 +1035,7 @@ public class AnthropicApiClientTests : IntegrationTest
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 CreateMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError()
{
@@ -1058,7 +1061,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task CreateMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError()
{
@@ -1078,7 +1081,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task CreateMessageBatchAsync_WhenCalledRequestSucceedsAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse()
{
@@ -1153,7 +1156,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Value.ResultsUrl.Should()
.Be("https://api.anthropic.com/v1/messages/batches/msgbatch_013Zva2CMHLNnXjNJJKqJ2EF/results");
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledAndErrorReturned_ItShouldHandleError()
{
@@ -1179,7 +1182,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledRequestFailsAndErrorCanNotBeDeserialized_ItShouldReturnUnknownError()
{
@@ -1199,7 +1202,7 @@ public class AnthropicApiClientTests : IntegrationTest
result.Error.Should().BeOfType();
result.Error.Error.Should().BeOfType();
}
-
+
[Fact]
public async Task GetMessageBatchAsync_WhenCalledAndCanNotDeserializeResponse_ItShouldReturnEmptyResponse()
{
@@ -1218,4 +1221,30 @@ public class AnthropicApiClientTests : IntegrationTest
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType();
}
+
+ [Fact]
+ public async Task GetMessageBatchResultsAsync_WhenCalledAndSuccessful_ItShouldReturnAllBatchResults()
+ {
+ var batchId = "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF";
+ var batchResultsPath = TestFileHelper.GetTestFilePath("batch_results.jsonl");
+ var batchResultsText = await File.ReadAllTextAsync(batchResultsPath);
+ var batchResults = batchResultsText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+ var expectedResults = batchResults.Select(Deserialize);
+
+ _mockHttpMessageHandler
+ .WhenGetMessageBatchResultsRequest(batchId)
+ .Respond(
+ HttpStatusCode.OK,
+ "application/x-jsonl",
+ batchResultsText
+ );
+
+ var result = await Client.GetMessageBatchResultsAsync(batchId);
+
+ result.IsSuccess.Should().BeTrue();
+
+ var actualResults = await result.Value.ToListAsync();
+
+ actualResults.Should().BeEquivalentTo(expectedResults);
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
index b69a60d..90653d4 100644
--- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
+++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs
@@ -1,6 +1,8 @@
+using AnthropicClient.Tests.Unit;
+
namespace AnthropicClient.Tests.Integration;
-public class IntegrationTest
+public class IntegrationTest : SerializationTest
{
protected readonly MockHttpMessageHandler _mockHttpMessageHandler = new();
protected AnthropicApiClient Client => CreateClient();
@@ -71,10 +73,16 @@ 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}");
}
+
+ public static MockedRequest WhenGetMessageBatchResultsRequest(this MockHttpMessageHandler mockHttpMessageHandler, string batchId)
+ {
+ return mockHttpMessageHandler
+ .SetupBaseRequest(HttpMethod.Get, $"{MessageBatchesEndpoint}/{batchId}/results");
+ }
}
\ No newline at end of file
diff --git a/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs
new file mode 100644
index 0000000..5023018
--- /dev/null
+++ b/tests/AnthropicClient.Tests/Unit/Models/MessageBatchResultItemTests.cs
@@ -0,0 +1,14 @@
+namespace AnthropicClient.Tests.Unit.Models;
+
+public class MessageBatchResultItemTests
+{
+ [Fact]
+ public void Constructor_WhenCalled_ItShouldReturnInstanceWithPropertiesSet()
+ {
+ var result = new MessageBatchResultItem();
+
+ result.Should().BeOfType();
+ result.CustomId.Should().BeEmpty();
+ result.Result.Should().Be(default);
+ }
+}
\ No newline at end of file