From a4784fd5dbdeb299f61d1ff58ebf54287da48630 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:54:00 -0500 Subject: [PATCH] feat: add models for streaming messages --- src/AnthropicClient/AnthropicClient.csproj | 1 + .../Json/ContentDeltaConverter.cs | 28 ++++ .../Json/EventDataConverter.cs | 34 +++++ .../Json/JsonSerializationOptions.cs | 2 + src/AnthropicClient/Models/AnthropicEvent.cs | 20 +++ .../Models/ChatMessageRequest.cs | 109 ++------------- src/AnthropicClient/Models/ChatResponse.cs | 16 +-- src/AnthropicClient/Models/ChatUsage.cs | 4 +- src/AnthropicClient/Models/ContentDelta.cs | 11 ++ .../Models/ContentDeltaEventData.cs | 20 +++ .../Models/ContentDeltaType.cs | 7 + .../Models/ContentStartEventData.cs | 22 +++ .../Models/ContentStopEventData.cs | 18 +++ src/AnthropicClient/Models/ErrorEventData.cs | 18 +++ src/AnthropicClient/Models/EventData.cs | 13 ++ src/AnthropicClient/Models/EventType.cs | 13 ++ src/AnthropicClient/Models/JsonDelta.cs | 19 +++ .../Models/MessageDeltaEventData.cs | 40 ++++++ src/AnthropicClient/Models/MessageRequest.cs | 127 +++++++++++++++++- .../Models/MessageStartEventData.cs | 18 +++ .../Models/MessageStopEventData.cs | 8 ++ src/AnthropicClient/Models/PingEventData.cs | 8 ++ .../Models/StreamChatMessageRequest.cs | 58 ++++++++ src/AnthropicClient/Models/TextDelta.cs | 18 +++ .../Models/ToolResultContent.cs | 4 +- src/AnthropicClient/Models/ToolUseContent.cs | 6 +- .../EndToEnd/ClientTests.cs | 20 +++ 27 files changed, 551 insertions(+), 111 deletions(-) create mode 100644 src/AnthropicClient/Json/ContentDeltaConverter.cs create mode 100644 src/AnthropicClient/Json/EventDataConverter.cs create mode 100644 src/AnthropicClient/Models/AnthropicEvent.cs create mode 100644 src/AnthropicClient/Models/ContentDelta.cs create mode 100644 src/AnthropicClient/Models/ContentDeltaEventData.cs create mode 100644 src/AnthropicClient/Models/ContentDeltaType.cs create mode 100644 src/AnthropicClient/Models/ContentStartEventData.cs create mode 100644 src/AnthropicClient/Models/ContentStopEventData.cs create mode 100644 src/AnthropicClient/Models/ErrorEventData.cs create mode 100644 src/AnthropicClient/Models/EventData.cs create mode 100644 src/AnthropicClient/Models/EventType.cs create mode 100644 src/AnthropicClient/Models/JsonDelta.cs create mode 100644 src/AnthropicClient/Models/MessageDeltaEventData.cs create mode 100644 src/AnthropicClient/Models/MessageStartEventData.cs create mode 100644 src/AnthropicClient/Models/MessageStopEventData.cs create mode 100644 src/AnthropicClient/Models/PingEventData.cs create mode 100644 src/AnthropicClient/Models/StreamChatMessageRequest.cs create mode 100644 src/AnthropicClient/Models/TextDelta.cs diff --git a/src/AnthropicClient/AnthropicClient.csproj b/src/AnthropicClient/AnthropicClient.csproj index 9c4fd50..6e2b388 100644 --- a/src/AnthropicClient/AnthropicClient.csproj +++ b/src/AnthropicClient/AnthropicClient.csproj @@ -19,6 +19,7 @@ + diff --git a/src/AnthropicClient/Json/ContentDeltaConverter.cs b/src/AnthropicClient/Json/ContentDeltaConverter.cs new file mode 100644 index 0000000..e384b8a --- /dev/null +++ b/src/AnthropicClient/Json/ContentDeltaConverter.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; + +using AnthropicClient.Models; + +namespace AnthropicClient.Json; + +class ContentDeltaConverter : JsonConverter +{ + public override ContentDelta 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 + { + ContentDeltaType.TextDelta => JsonSerializer.Deserialize(root.GetRawText(), options)!, + ContentDeltaType.JsonDelta => JsonSerializer.Deserialize(root.GetRawText(), options)!, + _ => throw new JsonException($"Unknown content type: {type}") + }; + } + + public override void Write(Utf8JsonWriter writer, ContentDelta value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Json/EventDataConverter.cs b/src/AnthropicClient/Json/EventDataConverter.cs new file mode 100644 index 0000000..d11855b --- /dev/null +++ b/src/AnthropicClient/Json/EventDataConverter.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; + +using AnthropicClient.Models; + +namespace AnthropicClient.Json; + +class EventDataConverter : JsonConverter +{ + public override EventData 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 + { + EventType.Ping => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.Error => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.MessageStart => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.MessageDelta => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.MessageStop => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.ContentBlockStart => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.ContentBlockDelta => JsonSerializer.Deserialize(root.GetRawText(), options)!, + EventType.ContentBlockStop => JsonSerializer.Deserialize(root.GetRawText(), options)!, + _ => throw new JsonException($"Unknown content type: {type}") + }; + } + + public override void Write(Utf8JsonWriter writer, EventData value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Json/JsonSerializationOptions.cs b/src/AnthropicClient/Json/JsonSerializationOptions.cs index 15214b0..fd90328 100644 --- a/src/AnthropicClient/Json/JsonSerializationOptions.cs +++ b/src/AnthropicClient/Json/JsonSerializationOptions.cs @@ -14,6 +14,8 @@ static class JsonSerializationOptions new ContentConverter(), new ToolChoiceConverter(), new ErrorConverter(), + new EventDataConverter(), + new ContentDeltaConverter(), }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; diff --git a/src/AnthropicClient/Models/AnthropicEvent.cs b/src/AnthropicClient/Models/AnthropicEvent.cs new file mode 100644 index 0000000..91bab77 --- /dev/null +++ b/src/AnthropicClient/Models/AnthropicEvent.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public record AnthropicEvent +{ + public string Type { get; init; } = string.Empty; + public EventData Data { get; init; } = default!; + + [JsonConstructor] + internal AnthropicEvent() + { + } + + public AnthropicEvent(string type, EventData data) + { + Type = type; + Data = data; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ChatMessageRequest.cs b/src/AnthropicClient/Models/ChatMessageRequest.cs index 6a54b95..895565a 100644 --- a/src/AnthropicClient/Models/ChatMessageRequest.cs +++ b/src/AnthropicClient/Models/ChatMessageRequest.cs @@ -1,7 +1,5 @@ using System.Text.Json.Serialization; -using AnthropicClient.Utils; - namespace AnthropicClient.Models; /// @@ -9,66 +7,8 @@ namespace AnthropicClient.Models; /// public class ChatMessageRequest : MessageRequest { - /// - /// Gets the model ID to use for the request. - /// - public string Model { get; init; } = string.Empty; - - /// - /// Gets the system ID to use for the request. - /// - public string? System { get; init; } = null; - - /// - /// Gets the messages to send to the model. - /// - public List Messages { get; init; } = []; - - /// - /// Gets the maximum number of tokens to generate. - /// - [JsonPropertyName("max_tokens")] - public int MaxTokens { get; init; } = 1024; - - /// - /// Gets the metadata to include with the request. - /// - public Dictionary? Metadata { get; init; } = null; - - /// - /// Gets the prompt stop sequences. - /// - [JsonPropertyName("stop_sequences")] - public List StopSequences { get; init; } = []; - - /// - /// Gets the temperature to use for the request. - /// - public decimal Temperature { get; init; } = 0.0m; - - /// - /// Gets the top-K value to use for the request. - /// - public int? TopK { get; init; } = null; - - /// - /// Gets the top-P value to use for the request. - /// - public decimal? TopP { get; init; } = null; - - /// - /// Gets the tool choice mode to use for the request. - /// - [JsonPropertyName("tool_choice")] - public ToolChoice? ToolChoice { get; init; } = null; - - /// - /// Gets the tools to use for the request. - /// - public List? Tools { get; init; } = null; - [JsonConstructor] - internal ChatMessageRequest() : base(false) { } + internal ChatMessageRequest() : base() { } /// /// Initializes a new instance of the class. @@ -100,40 +40,19 @@ public class ChatMessageRequest : MessageRequest decimal? topP = null, ToolChoice? toolChoice = null, List? tools = null - ) : base(false) + ) : base( + model, + messages, + maxTokens, + system, + metadata, + temperature, + topK, + topP, + toolChoice, + tools, + false + ) { - ArgumentValidator.ThrowIfNull(model, nameof(model)); - ArgumentValidator.ThrowIfNull(messages, nameof(messages)); - - if (AnthropicModels.IsValidModel(model) is false) - { - throw new ArgumentException($"Invalid model ID: {model}"); - } - - if (messages.Count < 1) - { - throw new ArgumentException("Messages must contain at least one message"); - } - - if (maxTokens < 1) - { - throw new ArgumentException($"Invalid max tokens: {maxTokens}"); - } - - if (temperature < 0.0m || temperature > 1.0m) - { - throw new ArgumentException($"Invalid temperature: {temperature}"); - } - - Model = model; - Messages = messages; - MaxTokens = maxTokens; - System = system; - Metadata = metadata; - Temperature = temperature; - TopK = topK; - TopP = topP; - ToolChoice = toolChoice; - Tools = tools; } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/ChatResponse.cs b/src/AnthropicClient/Models/ChatResponse.cs index b95e461..aa33a1b 100644 --- a/src/AnthropicClient/Models/ChatResponse.cs +++ b/src/AnthropicClient/Models/ChatResponse.cs @@ -10,42 +10,42 @@ public class ChatResponse /// /// Gets the ID of the chat response. /// - public string Id { get; set; } = string.Empty; + public string Id { get; init; } = string.Empty; /// /// Gets the model used for the chat response. /// - public string Model { get; set; } = string.Empty; + public string Model { get; init; } = string.Empty; /// /// Gets the role of the chat response. /// - public string Role { get; set; } = string.Empty; + public string Role { get; init; } = string.Empty; /// /// Gets the stop reason of the chat response. /// [JsonPropertyName("stop_reason")] - public string StopReason { get; set; } = string.Empty; + public string StopReason { get; init; } = string.Empty; /// /// Gets the stop sequence of the chat response. /// [JsonPropertyName("stop_sequence")] - public string StopSequence { get; set; } = string.Empty; + public string StopSequence { get; init; } = string.Empty; /// /// Gets the type of the chat response. /// - public string Type { get; set; } = string.Empty; + public string Type { get; init; } = string.Empty; /// /// Gets the usage of the chat response. /// - public ChatUsage Usage { get; set; } = new(); + public ChatUsage Usage { get; init; } = new(); /// /// Gets the contents of the chat response. /// - public List Content { get; set; } = []; + public List Content { get; init; } = []; } \ No newline at end of file diff --git a/src/AnthropicClient/Models/ChatUsage.cs b/src/AnthropicClient/Models/ChatUsage.cs index 73312ee..1dd7749 100644 --- a/src/AnthropicClient/Models/ChatUsage.cs +++ b/src/AnthropicClient/Models/ChatUsage.cs @@ -11,11 +11,11 @@ public class ChatUsage /// Gets the number of input tokens used. /// [JsonPropertyName("input_tokens")] - public int InputTokens { get; set; } + public int InputTokens { get; init; } /// /// Gets the number of output tokens used. /// [JsonPropertyName("output_tokens")] - public int OutputTokens { get; set; } + public int OutputTokens { get; init; } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/ContentDelta.cs b/src/AnthropicClient/Models/ContentDelta.cs new file mode 100644 index 0000000..fbc0484 --- /dev/null +++ b/src/AnthropicClient/Models/ContentDelta.cs @@ -0,0 +1,11 @@ +namespace AnthropicClient.Models; + +public abstract class ContentDelta +{ + public string Type { get; init; } + + protected ContentDelta(string type) + { + Type = type; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ContentDeltaEventData.cs b/src/AnthropicClient/Models/ContentDeltaEventData.cs new file mode 100644 index 0000000..4439a8e --- /dev/null +++ b/src/AnthropicClient/Models/ContentDeltaEventData.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class ContentDeltaEventData : EventData +{ + public int Index { get; init; } + public ContentDelta Delta { get; init; } = default!; + + [JsonConstructor] + internal ContentDeltaEventData() : base(EventType.ContentBlockDelta) + { + } + + public ContentDeltaEventData(int index, ContentDelta delta) : base(EventType.ContentBlockDelta) + { + Index = index; + Delta = delta; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ContentDeltaType.cs b/src/AnthropicClient/Models/ContentDeltaType.cs new file mode 100644 index 0000000..b5096f3 --- /dev/null +++ b/src/AnthropicClient/Models/ContentDeltaType.cs @@ -0,0 +1,7 @@ +namespace AnthropicClient.Models; + +public static class ContentDeltaType +{ + public const string TextDelta = "text_delta"; + public const string JsonDelta = "input_json_delta"; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ContentStartEventData.cs b/src/AnthropicClient/Models/ContentStartEventData.cs new file mode 100644 index 0000000..1edfa2e --- /dev/null +++ b/src/AnthropicClient/Models/ContentStartEventData.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class ContentStartEventData : EventData +{ + public int Index { get; init; } + + [JsonPropertyName("content_block")] + public Content ContentBlock { get; init; } + + [JsonConstructor] + internal ContentStartEventData() : base(EventType.ContentBlockStart) + { + } + + public ContentStartEventData(int index, Content contentBlock) : base(EventType.ContentBlockStart) + { + Index = index; + ContentBlock = contentBlock; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ContentStopEventData.cs b/src/AnthropicClient/Models/ContentStopEventData.cs new file mode 100644 index 0000000..b0ac609 --- /dev/null +++ b/src/AnthropicClient/Models/ContentStopEventData.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class ContentStopEventData : EventData +{ + public int Index { get; init; } + + [JsonConstructor] + internal ContentStopEventData() : base(EventType.ContentBlockStop) + { + } + + public ContentStopEventData(int index) : base(EventType.ContentBlockStop) + { + Index = index; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ErrorEventData.cs b/src/AnthropicClient/Models/ErrorEventData.cs new file mode 100644 index 0000000..a0b0af2 --- /dev/null +++ b/src/AnthropicClient/Models/ErrorEventData.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class ErrorEventData : EventData +{ + public Error Error { get; init; } = default!; + + [JsonConstructor] + internal ErrorEventData() : base(EventType.Error) + { + } + + public ErrorEventData(Error error) : base(EventType.Error) + { + Error = error; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/EventData.cs b/src/AnthropicClient/Models/EventData.cs new file mode 100644 index 0000000..e73d531 --- /dev/null +++ b/src/AnthropicClient/Models/EventData.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public abstract class EventData +{ + public string Type { get; init; } + + protected EventData(string type) + { + Type = type; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/EventType.cs b/src/AnthropicClient/Models/EventType.cs new file mode 100644 index 0000000..a652168 --- /dev/null +++ b/src/AnthropicClient/Models/EventType.cs @@ -0,0 +1,13 @@ +namespace AnthropicClient.Models; + +public static class EventType +{ + public const string Error = "error"; + public const string Ping = "ping"; + public const string MessageStart = "message_start"; + public const string MessageDelta = "message_delta"; + public const string MessageStop = "message_stop"; + public const string ContentBlockStart = "content_block_start"; + public const string ContentBlockDelta = "content_block_delta"; + public const string ContentBlockStop = "content_block_stop"; +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/JsonDelta.cs b/src/AnthropicClient/Models/JsonDelta.cs new file mode 100644 index 0000000..80c8b00 --- /dev/null +++ b/src/AnthropicClient/Models/JsonDelta.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class JsonDelta : ContentDelta +{ + [JsonPropertyName("partial_json")] + public string PartialJson { get; init; } = string.Empty; + + [JsonConstructor] + internal JsonDelta() : base(ContentDeltaType.JsonDelta) + { + } + + public JsonDelta(string partialJson) : base(ContentDeltaType.JsonDelta) + { + PartialJson = partialJson; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageDeltaEventData.cs b/src/AnthropicClient/Models/MessageDeltaEventData.cs new file mode 100644 index 0000000..240e307 --- /dev/null +++ b/src/AnthropicClient/Models/MessageDeltaEventData.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class MessageDeltaEventData : EventData +{ + public MessageDelta Delta { get; init; } + public ChatUsage Usage { get; init; } + + [JsonConstructor] + internal MessageDeltaEventData() : base(EventType.MessageDelta) + { + } + + public MessageDeltaEventData(MessageDelta delta, ChatUsage usage) : base(EventType.MessageDelta) + { + Delta = delta; + Usage = usage; + } +} + +public class MessageDelta +{ + [JsonPropertyName("stop_reason")] + public string StopReason { get; init; } = string.Empty; + + [JsonPropertyName("stop_sequence")] + public string StopSequence { get; init; } = string.Empty; + + [JsonConstructor] + internal MessageDelta() + { + } + + public MessageDelta(string stopReason, string stopSequence) + { + StopReason = stopReason; + StopSequence = stopSequence; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageRequest.cs b/src/AnthropicClient/Models/MessageRequest.cs index a676c4e..7beeeaa 100644 --- a/src/AnthropicClient/Models/MessageRequest.cs +++ b/src/AnthropicClient/Models/MessageRequest.cs @@ -1,3 +1,7 @@ +using System.Text.Json.Serialization; + +using AnthropicClient.Utils; + namespace AnthropicClient.Models; /// @@ -5,18 +9,139 @@ namespace AnthropicClient.Models; /// public abstract class MessageRequest { + /// + /// Gets the model ID to use for the request. + /// + public string Model { get; init; } = string.Empty; + + /// + /// Gets the system ID to use for the request. + /// + public string? System { get; init; } = null; + + /// + /// Gets the messages to send to the model. + /// + public List Messages { get; init; } = []; + + /// + /// Gets the maximum number of tokens to generate. + /// + [JsonPropertyName("max_tokens")] + public int MaxTokens { get; init; } = 1024; + + /// + /// Gets the metadata to include with the request. + /// + public Dictionary? Metadata { get; init; } = null; + + /// + /// Gets the prompt stop sequences. + /// + [JsonPropertyName("stop_sequences")] + public List StopSequences { get; init; } = []; + + /// + /// Gets the temperature to use for the request. + /// + public decimal Temperature { get; init; } = 0.0m; + + /// + /// Gets the top-K value to use for the request. + /// + public int? TopK { get; init; } = null; + + /// + /// Gets the top-P value to use for the request. + /// + public decimal? TopP { get; init; } = null; + + /// + /// Gets the tool choice mode to use for the request. + /// + [JsonPropertyName("tool_choice")] + public ToolChoice? ToolChoice { get; init; } = null; + + /// + /// Gets the tools to use for the request. + /// + public List? Tools { get; init; } = null; + /// /// Gets a value indicating whether the message should be streamed. /// public bool Stream { get; init; } + [JsonConstructor] + internal MessageRequest() { } + /// /// Initializes a new instance of the class. /// + /// The model ID to use for the request. + /// The messages to send to the model. + /// The maximum number of tokens to generate. + /// The system ID to use for the request. + /// The metadata to include with the request. + /// The temperature to use for the request. + /// The top-K value to use for the request. + /// The top-P value to use for the request. + /// The tool choice mode to use for the request. + /// The tools to use for the request. /// A value indicating whether the message should be streamed. + /// Thrown when the model ID is invalid. + /// Thrown when the model or messages is null. + /// Thrown when the messages contain no messages. + /// Thrown when the max tokens is less than one. + /// Thrown when the temperature is less than zero or greater than one. /// A new instance of the class. - public MessageRequest(bool stream = false) + protected MessageRequest( + string model, + List messages, + int maxTokens = 1024, + string? system = null, + Dictionary? metadata = null, + decimal temperature = 0.0m, + int? topK = null, + decimal? topP = null, + ToolChoice? toolChoice = null, + List? tools = null, + bool stream = false + ) { + ArgumentValidator.ThrowIfNull(model, nameof(model)); + ArgumentValidator.ThrowIfNull(messages, nameof(messages)); + + if (AnthropicModels.IsValidModel(model) is false) + { + throw new ArgumentException($"Invalid model ID: {model}"); + } + + if (messages.Count < 1) + { + throw new ArgumentException("Messages must contain at least one message"); + } + + if (maxTokens < 1) + { + throw new ArgumentException($"Invalid max tokens: {maxTokens}"); + } + + if (temperature < 0.0m || temperature > 1.0m) + { + throw new ArgumentException($"Invalid temperature: {temperature}"); + } + + Model = model; + Messages = messages; + MaxTokens = maxTokens; + System = system; + Metadata = metadata; + Temperature = temperature; + TopK = topK; + TopP = topP; + ToolChoice = toolChoice; + Tools = tools; Stream = stream; } } \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageStartEventData.cs b/src/AnthropicClient/Models/MessageStartEventData.cs new file mode 100644 index 0000000..16b4451 --- /dev/null +++ b/src/AnthropicClient/Models/MessageStartEventData.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class MessageStartEventData : EventData +{ + public ChatMessage Message { get; init; } = new(); + + [JsonConstructor] + internal MessageStartEventData() : base(EventType.MessageStart) + { + } + + public MessageStartEventData(ChatMessage message) : base(EventType.MessageStart) + { + Message = message; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/MessageStopEventData.cs b/src/AnthropicClient/Models/MessageStopEventData.cs new file mode 100644 index 0000000..ed781e6 --- /dev/null +++ b/src/AnthropicClient/Models/MessageStopEventData.cs @@ -0,0 +1,8 @@ +namespace AnthropicClient.Models; + +public class MessageStopEventData : EventData +{ + public MessageStopEventData() : base(EventType.MessageStop) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/PingEventData.cs b/src/AnthropicClient/Models/PingEventData.cs new file mode 100644 index 0000000..e288154 --- /dev/null +++ b/src/AnthropicClient/Models/PingEventData.cs @@ -0,0 +1,8 @@ +namespace AnthropicClient.Models; + +public class PingEventData : EventData +{ + public PingEventData() : base(EventType.Ping) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/StreamChatMessageRequest.cs b/src/AnthropicClient/Models/StreamChatMessageRequest.cs new file mode 100644 index 0000000..bcf50fb --- /dev/null +++ b/src/AnthropicClient/Models/StreamChatMessageRequest.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +/// +/// Represents a chat message request. +/// +public class StreamChatMessageRequest : MessageRequest +{ + [JsonConstructor] + internal StreamChatMessageRequest() : base() { } + + /// + /// Initializes a new instance of the class. + /// + /// The model ID to use for the request. + /// The messages to send to the model. + /// The maximum number of tokens to generate. + /// The system ID to use for the request. + /// The metadata to include with the request. + /// The temperature to use for the request. + /// The top-K value to use for the request. + /// The top-P value to use for the request. + /// The tool choice mode to use for the request. + /// The tools to use for the request. + /// Thrown when the model ID is invalid. + /// Thrown when the model or messages is null. + /// Thrown when the messages contain no messages. + /// Thrown when the max tokens is less than one. + /// Thrown when the temperature is less than zero or greater than one. + /// A new instance of the class. + public StreamChatMessageRequest( + string model, + List messages, + int maxTokens = 1024, + string? system = null, + Dictionary? metadata = null, + decimal temperature = 0.0m, + int? topK = null, + decimal? topP = null, + ToolChoice? toolChoice = null, + List? tools = null + ) : base( + model, + messages, + maxTokens, + system, + metadata, + temperature, + topK, + topP, + toolChoice, + tools, + true + ) + { + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/TextDelta.cs b/src/AnthropicClient/Models/TextDelta.cs new file mode 100644 index 0000000..0768ca2 --- /dev/null +++ b/src/AnthropicClient/Models/TextDelta.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace AnthropicClient.Models; + +public class TextDelta : ContentDelta +{ + public string Text { get; set; } = string.Empty; + + [JsonConstructor] + internal TextDelta() : base(ContentDeltaType.TextDelta) + { + } + + public TextDelta(string text) : base(ContentDeltaType.TextDelta) + { + Text = text; + } +} \ No newline at end of file diff --git a/src/AnthropicClient/Models/ToolResultContent.cs b/src/AnthropicClient/Models/ToolResultContent.cs index f8b8f82..b49f918 100644 --- a/src/AnthropicClient/Models/ToolResultContent.cs +++ b/src/AnthropicClient/Models/ToolResultContent.cs @@ -13,12 +13,12 @@ public class ToolResultContent : Content /// Gets the tool use ID of the content. /// [JsonPropertyName("tool_use_id")] - public string ToolUseId { get; set; } = string.Empty; + public string ToolUseId { get; init; } = string.Empty; /// /// Gets the content of the tool result. /// - public string Content { get; set; } = string.Empty; + public string Content { get; init; } = string.Empty; [JsonConstructor] internal ToolResultContent() : base(ContentType.ToolResult) { } diff --git a/src/AnthropicClient/Models/ToolUseContent.cs b/src/AnthropicClient/Models/ToolUseContent.cs index aef8668..b09dcd7 100644 --- a/src/AnthropicClient/Models/ToolUseContent.cs +++ b/src/AnthropicClient/Models/ToolUseContent.cs @@ -8,17 +8,17 @@ public class ToolUseContent : Content /// /// Gets the ID of the tool use. /// - public string Id { get; set; } = string.Empty; + public string Id { get; init; } = string.Empty; /// /// Gets the name of the tool. /// - public string Name { get; set; } = string.Empty; + public string Name { get; init; } = string.Empty; /// /// Gets the input of the tool. /// - public Dictionary Input { get; set; } = []; + public Dictionary Input { get; init; } = []; /// /// Initializes a new instance of the class. diff --git a/tests/AnthropicClient.Tests/EndToEnd/ClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/ClientTests.cs index d51f0ba..aee03d6 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/ClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/ClientTests.cs @@ -18,4 +18,24 @@ public class ClientTests( result.IsSuccess.Should().BeTrue(); result.Value.Should().BeOfType(); } + + [Fact] + public async Task CreateChatMessage_WhenCalledWithStreamRequest_IteratesOverChatResponse() + { + var request = new StreamChatMessageRequest( + model: AnthropicModels.Claude3Haiku, + messages: [new(MessageRole.User, [new TextContent("Hello!")])] + ); + + var response = _client.CreateChatMessageAsync(request); + + var events = new List(); + + await foreach (var e in response) + { + events.Add(e); + } + + events.Should().NotBeEmpty(); + } } \ No newline at end of file