From 422719f509fe456cd808cad78473f3b76e9a94d2 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:53:36 -0500 Subject: [PATCH] fix: move client to top level namespace --- src/AnthropicClient/{Models => }/Client.cs | 77 +++++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) rename src/AnthropicClient/{Models => }/Client.cs (54%) diff --git a/src/AnthropicClient/Models/Client.cs b/src/AnthropicClient/Client.cs similarity index 54% rename from src/AnthropicClient/Models/Client.cs rename to src/AnthropicClient/Client.cs index ba00ae2..a3cf0b8 100644 --- a/src/AnthropicClient/Models/Client.cs +++ b/src/AnthropicClient/Client.cs @@ -4,8 +4,9 @@ using System.Text.Json; using AnthropicClient.Json; using AnthropicClient.Utils; +using AnthropicClient.Models; -namespace AnthropicClient.Models; +namespace AnthropicClient; /// /// Represents a client for interacting with the Anthropic API. @@ -18,6 +19,13 @@ public interface IClient /// The chat message request to create. /// A task that represents the asynchronous operation. The task result contains the chat response as an . Task> CreateChatMessageAsync(ChatMessageRequest request); + + /// + /// Creates a chat message asynchronously and streams the response. + /// + /// The chat message request to create. + /// An asynchronous enumerable that yields the chat response line by line. + IAsyncEnumerable CreateChatMessageAsync(StreamChatMessageRequest request); } /// @@ -62,8 +70,7 @@ public class Client : IClient /// public async Task> CreateChatMessageAsync(ChatMessageRequest request) { - var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType); - var response = await _httpClient.PostAsync(MessagesEndpoint, requestContent); + var response = await SendRequestAsync(request); var requestId = GetRequestId(response); var responseContent = await response.Content.ReadAsStringAsync(); @@ -77,6 +84,70 @@ public class Client : IClient return AnthropicResult.Success(chatResponse, requestId); } + /// + public async IAsyncEnumerable CreateChatMessageAsync(StreamChatMessageRequest request) + { + var response = await SendRequestAsync(request); + var requestId = GetRequestId(response); + + using var responseContent = await response.Content.ReadAsStreamAsync(); + using var streamReader = new StreamReader(responseContent); + + var currentEvent = new AnthropicEvent(); + + // TODO: I'd like to emit custom events unique to this client + // - "content_block_complete" + // - provides the complete content block that was streamed + // - useful for streaming in text, but then handling tool calls with their entire input + + // will need to keep track of current content so that we can build it up as relevant + // deltas are streamed in + + // will want to capture the content block start event + // then continue to build this up until we get to the content block stop event + // at that point we should have the complete block and we should be able to yield + // return the complete block and reset the current content block + + // event: content_block_start + // data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01T1x1fJ34qAmk2tNTrN7Up6","name":"get_weather","input":{}}} + + + do + { + var line = await streamReader.ReadLineAsync(); + + if (line is null) + { + break; + } + + if (line.StartsWith("event:")) + { + var eventType = line.Substring("event:".Length).Trim(); + currentEvent = currentEvent with { Type = eventType }; + } + + if (line.StartsWith("data:")) + { + var eventData = line.Substring("data:".Length).Trim(); + var eventDataJson = JsonSerializer.Deserialize(eventData, JsonSerializationOptions.DefaultOptions); + currentEvent = currentEvent with { Data = eventDataJson! }; + } + + if (line == string.Empty) + { + yield return currentEvent; + currentEvent = new AnthropicEvent(); + } + } while (true); + } + + private async Task SendRequestAsync(MessageRequest request) + { + var requestContent = new StringContent(Serialize(request), Encoding.UTF8, JsonContentType); + return await _httpClient.PostAsync(MessagesEndpoint, requestContent); + } + private string Serialize(T obj) => JsonSerializer.Serialize(obj, JsonSerializationOptions.DefaultOptions); private T? Deserialize(string json) => JsonSerializer.Deserialize(json, JsonSerializationOptions.DefaultOptions); private string GetRequestId(HttpResponseMessage response) => response.Headers.GetValues(RequestIdHeader).FirstOrDefault() ?? string.Empty;