diff --git a/OpenAI-DotNet-Tests/TestFixture_03_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_03_Threads.cs index 99e2d69d..3b74f3f2 100644 --- a/OpenAI-DotNet-Tests/TestFixture_03_Threads.cs +++ b/OpenAI-DotNet-Tests/TestFixture_03_Threads.cs @@ -260,6 +260,51 @@ public async Task Test_03_03_01_CreateRun_Streaming() var run = await thread.CreateRunAsync(assistant, streamEvent => { Console.WriteLine(streamEvent.ToJsonString()); + + switch (streamEvent) + { + case RunResponse runEvent: + Assert.NotNull(runEvent); + break; + case RunStepResponse runStepEvent: + Assert.NotNull(runStepEvent); + + switch (runStepEvent.Object) + { + case "thread.run.step.delta": + Assert.NotNull(runStepEvent.Delta); + break; + default: + Assert.IsNull(runStepEvent.Delta); + break; + } + break; + case ThreadResponse threadEvent: + Assert.NotNull(threadEvent); + break; + case MessageResponse messageEvent: + Assert.NotNull(messageEvent); + + switch (messageEvent.Object) + { + case "thread.message.delta": + Assert.NotNull(messageEvent.Delta); + Console.WriteLine($"{messageEvent.Object}: \"{messageEvent.Delta.PrintContent()}\""); + break; + default: + Console.WriteLine($"{messageEvent.Object}: \"{messageEvent.PrintContent()}\""); + Assert.IsNull(messageEvent.Delta); + break; + } + break; + case Error errorEvent: + Assert.NotNull(errorEvent); + break; + //default: + // handle event not already processed by library + // var @event = JsonSerializer.Deserialize(streamEvent.ToJsonString()); + //break; + } }); Assert.IsNotNull(run); diff --git a/OpenAI-DotNet/OpenAI-DotNet.csproj b/OpenAI-DotNet/OpenAI-DotNet.csproj index 058a7f0a..6653566e 100644 --- a/OpenAI-DotNet/OpenAI-DotNet.csproj +++ b/OpenAI-DotNet/OpenAI-DotNet.csproj @@ -28,8 +28,12 @@ More context [on Roger Pincombe's blog](https://rogerpincombe.com/openai-dotnet- OpenAI-DotNet.pfx True True - 8.0.2 + 8.0.3 +Version 8.0.3 +- Fixed Thread.MessageResponse and Thread.RunStepResponse Delta stream event objects not being properly populated +- Added Thread.MessageDelta.PrintContent() +- Added additional unit tests for delta objects Version 8.0.2 - Fixed Thread.Message.Attachement serialization - Fixed CreateAssistantRequest to properly copy all override assistant properties diff --git a/OpenAI-DotNet/Threads/MessageDelta.cs b/OpenAI-DotNet/Threads/MessageDelta.cs index 0f280608..42654fd9 100644 --- a/OpenAI-DotNet/Threads/MessageDelta.cs +++ b/OpenAI-DotNet/Threads/MessageDelta.cs @@ -1,6 +1,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Collections.Generic; +using System.Linq; using System.Text.Json.Serialization; namespace OpenAI.Threads @@ -9,10 +10,20 @@ public sealed class MessageDelta { [JsonInclude] [JsonPropertyName("role")] - public Role Role { get; private set; } + public Role Role { get; internal set; } [JsonInclude] [JsonPropertyName("content")] public IReadOnlyList Content { get; private set; } + + /// + /// Formats all of the items into a single string, + /// putting each item on a new line. + /// + /// of all . + public string PrintContent() + => Content == null + ? string.Empty + : string.Join("\n", Content.Select(c => c?.ToString())); } } diff --git a/OpenAI-DotNet/Threads/MessageResponse.cs b/OpenAI-DotNet/Threads/MessageResponse.cs index ec42d447..a24e55b7 100644 --- a/OpenAI-DotNet/Threads/MessageResponse.cs +++ b/OpenAI-DotNet/Threads/MessageResponse.cs @@ -181,13 +181,30 @@ internal void AppendFrom(MessageResponse other) { if (other == null) { return; } + if (!string.IsNullOrWhiteSpace(Id)) + { + if (Id != other.Id) + { + throw new InvalidOperationException("Attempting to append a different object than the original!"); + } + } + else + { + Id = other.Id; + } + + Object = other.Object; + if (other.Delta != null) { - if (Role == 0 && - other.Delta.Role > 0) + if (Role == 0 && other.Delta.Role > 0) { Role = other.Delta.Role; } + else if (other.Delta.Role == 0 && Role > 0) + { + other.Delta.Role = Role; + } if (other.Delta.Content != null) { @@ -195,10 +212,14 @@ internal void AppendFrom(MessageResponse other) content.AppendFrom(other.Delta.Content); } + Delta = other.Delta; + // bail early since we only care about the delta content return; } + Delta = null; + if (Role == 0 && other.Role > 0) { diff --git a/OpenAI-DotNet/Threads/RunResponse.cs b/OpenAI-DotNet/Threads/RunResponse.cs index 2a6c641b..7027c254 100644 --- a/OpenAI-DotNet/Threads/RunResponse.cs +++ b/OpenAI-DotNet/Threads/RunResponse.cs @@ -281,6 +281,7 @@ public IReadOnlyList Tools [JsonInclude] [JsonPropertyName("response_format")] [JsonConverter(typeof(ResponseFormatConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public ChatResponseFormat ResponseFormat { get; private set; } public static implicit operator string(RunResponse run) => run?.ToString(); @@ -291,6 +292,20 @@ internal void AppendFrom(RunResponse other) { if (other is null) { return; } + if (!string.IsNullOrWhiteSpace(Id)) + { + if (Id != other.Id) + { + throw new InvalidOperationException("Attempting to append a different object than the original!"); + } + } + else + { + Id = other.Id; + } + + Object = other.Object; + if (other.Status > 0) { Status = other.Status; diff --git a/OpenAI-DotNet/Threads/RunStepResponse.cs b/OpenAI-DotNet/Threads/RunStepResponse.cs index d502f7dd..de19c4c1 100644 --- a/OpenAI-DotNet/Threads/RunStepResponse.cs +++ b/OpenAI-DotNet/Threads/RunStepResponse.cs @@ -32,7 +32,7 @@ public RunStepResponse() { } [JsonInclude] [JsonPropertyName("delta")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] - public RunStepDelta Delta { get; } + public RunStepDelta Delta { get; private set; } /// /// The Unix timestamp (in seconds) for when the run step was created. @@ -191,6 +191,20 @@ internal void AppendFrom(RunStepResponse other) { if (other == null) { return; } + if (!string.IsNullOrWhiteSpace(Id)) + { + if (Id != other.Id) + { + throw new InvalidOperationException("Attempting to append a different object than the original!"); + } + } + else + { + Id = other.Id; + } + + Object = other.Object; + if (other.Delta != null) { if (other.Delta.StepDetails != null) @@ -205,10 +219,14 @@ internal void AppendFrom(RunStepResponse other) } } + Delta = other.Delta; + // don't update other fields if we are just appending Delta return; } + Delta = null; + if (other.CreatedAtUnixTimeSeconds.HasValue) { CreatedAtUnixTimeSeconds = other.CreatedAtUnixTimeSeconds; diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs index ab2e2177..08043854 100644 --- a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -433,6 +433,7 @@ private async Task StreamRunAsync(string endpoint, StringContent pa case "thread.run.cancelled": case "thread.run.expired": var partialRun = sseResponse.Deserialize(ssEvent, client); + if (run == null) { run = partialRun; @@ -452,6 +453,7 @@ private async Task StreamRunAsync(string endpoint, StringContent pa case "thread.run.step.cancelled": case "thread.run.step.expired": var partialRunStep = sseResponse.Deserialize(ssEvent, client); + if (runStep == null) { runStep = partialRunStep; @@ -469,6 +471,7 @@ private async Task StreamRunAsync(string endpoint, StringContent pa case "thread.message.completed": case "thread.message.incomplete": var partialMessage = sseResponse.Deserialize(ssEvent, client); + if (message == null) { message = partialMessage;