diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs index f5babc0..21ae076 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -236,7 +236,7 @@ public class AnthropicApiClientTests : IntegrationTest [Theory] [ClassData(typeof(EventTestData))] - public async Task CreateChatMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldReturnAllEvents(string eventString, AnthropicEvent anthropicEvent) + public async Task CreateChatMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldHandleAllEventTypes(string eventString, AnthropicEvent anthropicEvent) { _mockHttpMessageHandler .WhenCreateStreamMessageRequest() @@ -258,4 +258,72 @@ public class AnthropicApiClientTests : IntegrationTest e.Should().BeEquivalentTo(anthropicEvent); } + + [Fact] + public async Task CreateChatMessageAsync_WhenCalledAndMessageIsStreamed_ItShouldReturnAllExpectedEvents() + { + var eventStream = EventTestData.GetEventStream(); + var events = EventTestData.GetAllEvents(); + + _mockHttpMessageHandler + .WhenCreateStreamMessageRequest() + .Respond( + HttpStatusCode.OK, + "text/event-stream", + eventStream + ); + + var request = new StreamChatMessageRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + ] + ); + + var result = Client.CreateChatMessageAsync(request); + + var actualEvents = await result.ToListAsync(); + + actualEvents.Should().BeEquivalentTo(events); + } + + [Fact] + public async Task CreateChatMessageAsync_WhenCalledMessageIsStreamedAndToolProvide_ItShouldReturnToolCall() + { + var eventStream = EventTestData.GetEventStream(); + + _mockHttpMessageHandler + .WhenCreateStreamMessageRequest() + .Respond( + HttpStatusCode.OK, + "text/event-stream", + eventStream + ); + + var getWeather = (string location, string unit) => $"The weather in {location} is 72°{unit}"; + + var request = new StreamChatMessageRequest( + model: AnthropicModels.Claude35Sonnet, + messages: [ + new(MessageRole.User, [new TextContent("Hello!")]), + ], + tools: [ + Tool.CreateFromFunction("get_weather", "Gets the weather for a location", getWeather) + ] + ); + + var result = Client.CreateChatMessageAsync(request); + var msgCompleteEvent = await result + .Where(e => e.Type is EventType.MessageComplete) + .FirstAsync(); + + msgCompleteEvent.Data.Should().BeOfType(); + + var toolCall = msgCompleteEvent.Data.As().Message.ToolCall; + var toolCallResult = await toolCall!.InvokeAsync(); + + toolCallResult.IsSuccess.Should().BeTrue(); + toolCallResult.Error.Should().BeNull(); + toolCallResult.Value.Should().Be(getWeather("San Francisco, CA","fahrenheit")); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/EventTestData.cs b/tests/AnthropicClient.Tests/Integration/EventTestData.cs index 5bdc571..1ba2a56 100644 --- a/tests/AnthropicClient.Tests/Integration/EventTestData.cs +++ b/tests/AnthropicClient.Tests/Integration/EventTestData.cs @@ -2,6 +2,45 @@ namespace AnthropicClient.Tests.Integration; public class EventTestData : IEnumerable { + public static MemoryStream GetEventStream() + { + var events = new EventTestData().Select(x => x[0].ToString())!; + var content = string.Join("\n\n", events); + return new MemoryStream(Encoding.UTF8.GetBytes(content)); + } + public static List GetAllEvents() => new EventTestData().Select(x => x[1] as AnthropicEvent).Append(MessageCompleteEvent).ToList()!; + + public static readonly AnthropicEvent MessageCompleteEvent = new() + { + Type = EventType.MessageComplete, + Data = new MessageCompleteEventData( + new() + { + Id = "msg_014p7gG3wDgGV9EUtLvnow3U", + Type = "message", + Role = MessageRole.Assistant, + Model = AnthropicModels.Claude3Haiku, + StopSequence = null, + Usage = new ChatUsage { InputTokens = 472, OutputTokens = 91 }, + StopReason = "tool_use", + Content = [ + new TextContent("Okay, let's check the weather for San Francisco, CA:"), + new ToolUseContent() + { + Id = "toolu_01T1x1fJ34qAmk2tNTrN7Up6", + Name = "get_weather", + Input = new Dictionary + { + { "location", "San Francisco, CA" }, + { "unit", "fahrenheit" } + }, + }, + ] + }, + new() + ) + }; + public IEnumerator GetEnumerator() { yield return new object[] @@ -379,6 +418,248 @@ public class EventTestData : IEnumerable }, }, }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = "", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"location\":"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = "{\"location\":", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"San"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = " \"San", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Francisc"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = " Francisc", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"o,"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = "o,", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" CA\""}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = " CA\"", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":", "}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = ", ", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"unit\": \"fah"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = "\"unit\": \"fah", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_delta + data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"renheit\"}"}} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockDelta, + Data = new ContentDeltaEventData() + { + Index = 1, + Delta = new JsonDelta() + { + Type = "input_json_delta", + PartialJson = "renheit\"}", + }, + }, + }, + }; + + yield return new object[] + { + """ + event: content_block_stop + data: {"type":"content_block_stop","index":1} + """, + new AnthropicEvent() + { + Type = EventType.ContentBlockStop, + Data = new ContentStopEventData() + { + Index = 1, + }, + }, + }; + + yield return new object[] + { + """ + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":89}} + """, + new AnthropicEvent() + { + Type = EventType.MessageDelta, + Data = new MessageDeltaEventData() + { + Delta = new MessageDelta() + { + StopReason = "tool_use", + StopSequence = null, + }, + Usage = new ChatUsage() + { + OutputTokens = 89, + }, + }, + } + }; + + yield return new object[] + { + """ + event: message_stop + data: {"type":"message_stop"} + """, + new AnthropicEvent() + { + Type = EventType.MessageStop, + Data = new MessageStopEventData(), + }, + }; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();