From d665b61fc7ef1ada00a8ef5c00d1a47d276be032 Mon Sep 17 00:00:00 2001 From: Travis Wilson <35748617+trrwilson@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:05:08 -0700 Subject: [PATCH] Fix Assistants issues with Message role, Code logs (#81) --- CHANGELOG.md | 11 +- ...ple01_RetrievalAugmentedGenerationAsync.cs | 8 +- .../Example02b_FunctionCallingStreaming.cs | 1 + .../Example05_AssistantsWithVision.cs | 1 + .../Example05_AssistantsWithVisionAsync.cs | 1 + .../Assistants/AssistantClient.Convenience.cs | 8 +- src/Custom/Assistants/AssistantClient.cs | 6 + .../Assistants/AssistantCreationOptions.cs | 1 - .../Internal/GeneratorStubs.Internal.cs | 7 - ...InternalRunStepCodeInterpreterLogOutput.cs | 1 + .../Assistants/MessageCreationOptions.cs | 5 + .../RunStepCodeInterpreterOutput.cs | 2 +- .../Assistants/ThreadInitializationMessage.cs | 15 +- ...CodeInterpreterLogOutput.Serialization.cs} | 40 ++--- ...InternalRunStepCodeInterpreterLogOutput.cs | 29 +++ ...tepDetailsToolCallsCodeOutputLogsObject.cs | 29 --- .../Models/MessageCreationOptions.cs | 2 - ...StepCodeInterpreterOutput.Serialization.cs | 2 +- tests/Assistants/AssistantTests.cs | 167 ++++++++++++++++-- 19 files changed, 242 insertions(+), 94 deletions(-) rename src/Generated/Models/{InternalRunStepDetailsToolCallsCodeOutputLogsObject.Serialization.cs => InternalRunStepCodeInterpreterLogOutput.Serialization.cs} (59%) create mode 100644 src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.cs delete mode 100644 src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f9de1e..1000cd7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ ## Bugs Fixed -- ([#72](https://github.com/openai/openai-dotnet/issues/72)) Fixed `filename` request encoding in operations using `multipart/form-data`, including `files` and `audio` +- ([#72](https://github.com/openai/openai-dotnet/issues/72)) Fixed `filename` request encoding in operations using `multipart/form-data`, including `files` and `audio` (commit_hash) +- ([#79](https://github.com/openai/openai-dotnet/issues/79)) Fixed hard-coded `user` role for caller-created Assistants API messages on threads (commit_hash) +- Fixed non-streaming Assistants API run step details not reporting code interpreter logs when present + +## Breaking Changes + +**Assistants (beta)**: +- `AssistantClient.CreateMessage()` and the explicit constructor for `ThreadInitializationMessage` now require a `MessageRole` parameter. This properly enables the ability to create an Assistant message representing conversation history on a new thread. ## 2.0.0-beta.5 (2024-06-14) @@ -22,7 +29,7 @@ ## Breaking Changes -**Assistants**: +**Assistants (beta)**: - `InputQuote` is removed from Assistants `TextAnnotation` and `TextAnnotationUpdate`, per [openai/openai-openapi@dd73070b](https://github.com/openai/openai-openapi/commit/dd73070b1d507645d24c249a63ebebd3ec38c0cb) ([1af6569](https://github.com/openai/openai-dotnet/commit/1af6569e2ceae9d840b8826e42d7e3b2569b43f6)) ## Other Changes diff --git a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs index 68767bd1..6cde83f3 100644 --- a/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs +++ b/examples/Assistants/Example01_RetrievalAugmentedGenerationAsync.cs @@ -86,13 +86,7 @@ public async Task Example01_RetrievalAugmentedGenerationAsync() // Now we'll create a thread with a user query about the data already associated with the assistant, then run it ThreadCreationOptions threadOptions = new() { - InitialMessages = - { - new ThreadInitializationMessage(new List() - { - MessageContent.FromText("How well did product 113045 sell in February? Graph its trend over time."), - }), - }, + InitialMessages = { "How well did product 113045 sell in February? Graph its trend over time." } }; ThreadRun threadRun = await assistantClient.CreateThreadAndRunAsync(assistant.Id, threadOptions); diff --git a/examples/Assistants/Example02b_FunctionCallingStreaming.cs b/examples/Assistants/Example02b_FunctionCallingStreaming.cs index 616b5f74..cb3bf734 100644 --- a/examples/Assistants/Example02b_FunctionCallingStreaming.cs +++ b/examples/Assistants/Example02b_FunctionCallingStreaming.cs @@ -84,6 +84,7 @@ public async Task Example02b_FunctionCallingStreaming() AssistantThread thread = await client.CreateThreadAsync(); ThreadMessage message = await client.CreateMessageAsync( thread, + MessageRole.User, [ "What's the weather in San Francisco today and the likelihood it'll rain?" ]); diff --git a/examples/Assistants/Example05_AssistantsWithVision.cs b/examples/Assistants/Example05_AssistantsWithVision.cs index 61edd89a..e7322226 100644 --- a/examples/Assistants/Example05_AssistantsWithVision.cs +++ b/examples/Assistants/Example05_AssistantsWithVision.cs @@ -35,6 +35,7 @@ public void Example05_AssistantsWithVision() InitialMessages = { new ThreadInitializationMessage( + MessageRole.User, [ "Hello, assistant! Please compare these two images for me:", MessageContent.FromImageFileId(pictureOfAppleFile.Id), diff --git a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs index ba4b200b..68e50ed5 100644 --- a/examples/Assistants/Example05_AssistantsWithVisionAsync.cs +++ b/examples/Assistants/Example05_AssistantsWithVisionAsync.cs @@ -36,6 +36,7 @@ public async Task Example05_AssistantsWithVisionAsync() InitialMessages = { new ThreadInitializationMessage( + MessageRole.User, [ "Hello, assistant! Please compare these two images for me:", MessageContent.FromImageFileId(pictureOfAppleFile.Id), diff --git a/src/Custom/Assistants/AssistantClient.Convenience.cs b/src/Custom/Assistants/AssistantClient.Convenience.cs index e1021bd2..510bbee5 100644 --- a/src/Custom/Assistants/AssistantClient.Convenience.cs +++ b/src/Custom/Assistants/AssistantClient.Convenience.cs @@ -99,27 +99,31 @@ public virtual ClientResult DeleteThread(AssistantThread thread) /// Creates a new on an existing . /// /// The thread to associate the new message with. + /// The role to associate with the new message. /// The collection of items for the message. /// Additional options to apply to the new message. /// A new . public virtual Task> CreateMessageAsync( AssistantThread thread, + MessageRole role, IEnumerable content, MessageCreationOptions options = null) - => CreateMessageAsync(thread?.Id, content, options); + => CreateMessageAsync(thread?.Id, role, content, options); /// /// Creates a new on an existing . /// /// The thread to associate the new message with. + /// The role to associate with the new message. /// The collection of items for the message. /// Additional options to apply to the new message. /// A new . public virtual ClientResult CreateMessage( AssistantThread thread, + MessageRole role, IEnumerable content, MessageCreationOptions options = null) - => CreateMessage(thread?.Id, content, options); + => CreateMessage(thread?.Id, role, content, options); /// /// Returns a collection of instances from an existing . diff --git a/src/Custom/Assistants/AssistantClient.cs b/src/Custom/Assistants/AssistantClient.cs index 14eba1fe..15540914 100644 --- a/src/Custom/Assistants/AssistantClient.cs +++ b/src/Custom/Assistants/AssistantClient.cs @@ -280,18 +280,21 @@ public virtual ClientResult DeleteThread(string threadId, CancellationToke /// Creates a new on an existing . /// /// The ID of the thread to associate the new message with. + /// The role to associate with the new message. /// The collection of items for the message. /// Additional options to apply to the new message. /// A token that can be used to cancel this method call. /// A new . public virtual async Task> CreateMessageAsync( string threadId, + MessageRole role, IEnumerable content, MessageCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); options ??= new(); + options.Role = role; options.Content.Clear(); foreach (MessageContent contentItem in content) { @@ -307,18 +310,21 @@ public virtual async Task> CreateMessageAsync( /// Creates a new on an existing . /// /// The ID of the thread to associate the new message with. + /// The role to associate with the new message. /// The collection of items for the message. /// Additional options to apply to the new message. /// A token that can be used to cancel this method call. /// A new . public virtual ClientResult CreateMessage( string threadId, + MessageRole role, IEnumerable content, MessageCreationOptions options = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(threadId, nameof(threadId)); options ??= new(); + options.Role = role; options.Content.Clear(); foreach (MessageContent contentItem in content) { diff --git a/src/Custom/Assistants/AssistantCreationOptions.cs b/src/Custom/Assistants/AssistantCreationOptions.cs index 8603f029..d9fbe574 100644 --- a/src/Custom/Assistants/AssistantCreationOptions.cs +++ b/src/Custom/Assistants/AssistantCreationOptions.cs @@ -50,6 +50,5 @@ public AssistantCreationOptions() { Metadata = new ChangeTrackingDictionary(); Tools = new ChangeTrackingList(); - ToolResources = new(); } } \ No newline at end of file diff --git a/src/Custom/Assistants/Internal/GeneratorStubs.Internal.cs b/src/Custom/Assistants/Internal/GeneratorStubs.Internal.cs index a842463a..5a521d7c 100644 --- a/src/Custom/Assistants/Internal/GeneratorStubs.Internal.cs +++ b/src/Custom/Assistants/Internal/GeneratorStubs.Internal.cs @@ -207,13 +207,6 @@ internal readonly partial struct InternalListRunStepsResponseObject {} [CodeGenModel("RunStepDetailsToolCallsFileSearchObject")] internal partial class InternalRunStepFileSearchToolCallDetails { } -[CodeGenModel("RunStepDetailsToolCallsCodeOutputLogsObject")] -internal partial class InternalRunStepDetailsToolCallsCodeOutputLogsObject -{ - [CodeGenMember("Logs")] - internal string InternalLogs { get; } -} - [CodeGenModel("RunTruncationStrategyType")] internal readonly partial struct InternalRunTruncationStrategyType { } diff --git a/src/Custom/Assistants/Internal/InternalRunStepCodeInterpreterLogOutput.cs b/src/Custom/Assistants/Internal/InternalRunStepCodeInterpreterLogOutput.cs index 950c31ad..4d0a1d8e 100644 --- a/src/Custom/Assistants/Internal/InternalRunStepCodeInterpreterLogOutput.cs +++ b/src/Custom/Assistants/Internal/InternalRunStepCodeInterpreterLogOutput.cs @@ -1,6 +1,7 @@ namespace OpenAI.Assistants { /// Text output from the Code Interpreter tool call as part of a run step. + [CodeGenModel("RunStepDetailsToolCallsCodeOutputLogsObject")] internal partial class InternalRunStepCodeInterpreterLogOutput : RunStepCodeInterpreterOutput { /// The text output from the Code Interpreter tool call. diff --git a/src/Custom/Assistants/MessageCreationOptions.cs b/src/Custom/Assistants/MessageCreationOptions.cs index 050c13e2..4e0e1bd8 100644 --- a/src/Custom/Assistants/MessageCreationOptions.cs +++ b/src/Custom/Assistants/MessageCreationOptions.cs @@ -11,6 +11,11 @@ namespace OpenAI.Assistants; [CodeGenSerialization(nameof(Content), SerializationValueHook=nameof(SerializeContent))] public partial class MessageCreationOptions { + // CUSTOM: role is hidden, as this required property is promoted to a method parameter + + [CodeGenMember("Role")] + internal MessageRole Role { get; set; } + // CUSTOM: content is hidden to allow the promotion of required request information into top-level // method signatures. diff --git a/src/Custom/Assistants/RunStepCodeInterpreterOutput.cs b/src/Custom/Assistants/RunStepCodeInterpreterOutput.cs index ec17c2a1..8c0b7c7f 100644 --- a/src/Custom/Assistants/RunStepCodeInterpreterOutput.cs +++ b/src/Custom/Assistants/RunStepCodeInterpreterOutput.cs @@ -5,7 +5,7 @@ public abstract partial class RunStepCodeInterpreterOutput /// public string ImageFileId => AsInternalImage?.FileId; /// - public string Logs => AsInternalLogs?.Logs; + public string Logs => AsInternalLogs?.InternalLogs; private InternalRunStepDetailsToolCallsCodeOutputImageObject AsInternalImage => this as InternalRunStepDetailsToolCallsCodeOutputImageObject; private InternalRunStepCodeInterpreterLogOutput AsInternalLogs => this as InternalRunStepCodeInterpreterLogOutput; diff --git a/src/Custom/Assistants/ThreadInitializationMessage.cs b/src/Custom/Assistants/ThreadInitializationMessage.cs index 68f14ff5..60c44397 100644 --- a/src/Custom/Assistants/ThreadInitializationMessage.cs +++ b/src/Custom/Assistants/ThreadInitializationMessage.cs @@ -10,8 +10,10 @@ public partial class ThreadInitializationMessage : MessageCreationOptions /// /// The content items that should be included in the message, added to the thread being created. /// - public ThreadInitializationMessage(IEnumerable content) : base(content) - { } + public ThreadInitializationMessage(MessageRole role, IEnumerable content) : base(content) + { + Role = role; + } internal ThreadInitializationMessage(MessageCreationOptions baseOptions) : base(baseOptions.Role, baseOptions.Content, baseOptions.Attachments, baseOptions.Metadata, null) @@ -19,12 +21,13 @@ internal ThreadInitializationMessage(MessageCreationOptions baseOptions) /// /// Implicitly creates a new instance of from a single item of plain text - /// content. + /// content, assuming the role of . /// /// /// Using a in the position of a is equivalent to - /// using the constructor with a single - /// content instance. + /// using the constructor with + /// and a single content instance. /// - public static implicit operator ThreadInitializationMessage(string initializationMessage) => new([initializationMessage]); + public static implicit operator ThreadInitializationMessage(string initializationMessage) + => new(MessageRole.User, [initializationMessage]); } diff --git a/src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.Serialization.cs b/src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.Serialization.cs similarity index 59% rename from src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.Serialization.cs rename to src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.Serialization.cs index 17571b42..eced3a38 100644 --- a/src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.Serialization.cs +++ b/src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.Serialization.cs @@ -10,14 +10,14 @@ namespace OpenAI.Assistants { - internal partial class InternalRunStepDetailsToolCallsCodeOutputLogsObject : IJsonModel + internal partial class InternalRunStepCodeInterpreterLogOutput : IJsonModel { - void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) + void IJsonModel.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) { - var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; if (format != "J") { - throw new FormatException($"The model {nameof(InternalRunStepDetailsToolCallsCodeOutputLogsObject)} does not support writing '{format}' format."); + throw new FormatException($"The model {nameof(InternalRunStepCodeInterpreterLogOutput)} does not support writing '{format}' format."); } writer.WriteStartObject(); @@ -43,19 +43,19 @@ void IJsonModel.Write(Utf8J writer.WriteEndObject(); } - InternalRunStepDetailsToolCallsCodeOutputLogsObject IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) + InternalRunStepCodeInterpreterLogOutput IJsonModel.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options) { - var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; if (format != "J") { - throw new FormatException($"The model {nameof(InternalRunStepDetailsToolCallsCodeOutputLogsObject)} does not support reading '{format}' format."); + throw new FormatException($"The model {nameof(InternalRunStepCodeInterpreterLogOutput)} does not support reading '{format}' format."); } using JsonDocument document = JsonDocument.ParseValue(ref reader); - return DeserializeInternalRunStepDetailsToolCallsCodeOutputLogsObject(document.RootElement, options); + return DeserializeInternalRunStepCodeInterpreterLogOutput(document.RootElement, options); } - internal static InternalRunStepDetailsToolCallsCodeOutputLogsObject DeserializeInternalRunStepDetailsToolCallsCodeOutputLogsObject(JsonElement element, ModelReaderWriterOptions options = null) + internal static InternalRunStepCodeInterpreterLogOutput DeserializeInternalRunStepCodeInterpreterLogOutput(JsonElement element, ModelReaderWriterOptions options = null) { options ??= ModelSerializationExtensions.WireOptions; @@ -85,44 +85,44 @@ internal static InternalRunStepDetailsToolCallsCodeOutputLogsObject DeserializeI } } serializedAdditionalRawData = rawDataDictionary; - return new InternalRunStepDetailsToolCallsCodeOutputLogsObject(type, serializedAdditionalRawData, logs); + return new InternalRunStepCodeInterpreterLogOutput(type, serializedAdditionalRawData, logs); } - BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) + BinaryData IPersistableModel.Write(ModelReaderWriterOptions options) { - var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { case "J": return ModelReaderWriter.Write(this, options); default: - throw new FormatException($"The model {nameof(InternalRunStepDetailsToolCallsCodeOutputLogsObject)} does not support writing '{options.Format}' format."); + throw new FormatException($"The model {nameof(InternalRunStepCodeInterpreterLogOutput)} does not support writing '{options.Format}' format."); } } - InternalRunStepDetailsToolCallsCodeOutputLogsObject IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) + InternalRunStepCodeInterpreterLogOutput IPersistableModel.Create(BinaryData data, ModelReaderWriterOptions options) { - var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + var format = options.Format == "W" ? ((IPersistableModel)this).GetFormatFromOptions(options) : options.Format; switch (format) { case "J": { using JsonDocument document = JsonDocument.Parse(data); - return DeserializeInternalRunStepDetailsToolCallsCodeOutputLogsObject(document.RootElement, options); + return DeserializeInternalRunStepCodeInterpreterLogOutput(document.RootElement, options); } default: - throw new FormatException($"The model {nameof(InternalRunStepDetailsToolCallsCodeOutputLogsObject)} does not support reading '{options.Format}' format."); + throw new FormatException($"The model {nameof(InternalRunStepCodeInterpreterLogOutput)} does not support reading '{options.Format}' format."); } } - string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; + string IPersistableModel.GetFormatFromOptions(ModelReaderWriterOptions options) => "J"; - internal static new InternalRunStepDetailsToolCallsCodeOutputLogsObject FromResponse(PipelineResponse response) + internal static new InternalRunStepCodeInterpreterLogOutput FromResponse(PipelineResponse response) { using var document = JsonDocument.Parse(response.Content); - return DeserializeInternalRunStepDetailsToolCallsCodeOutputLogsObject(document.RootElement); + return DeserializeInternalRunStepCodeInterpreterLogOutput(document.RootElement); } internal override BinaryContent ToBinaryContent() diff --git a/src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.cs b/src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.cs new file mode 100644 index 00000000..a44f25f4 --- /dev/null +++ b/src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace OpenAI.Assistants +{ + internal partial class InternalRunStepCodeInterpreterLogOutput : RunStepCodeInterpreterOutput + { + internal InternalRunStepCodeInterpreterLogOutput(string internalLogs) + { + Argument.AssertNotNull(internalLogs, nameof(internalLogs)); + + Type = "logs"; + InternalLogs = internalLogs; + } + + internal InternalRunStepCodeInterpreterLogOutput(string type, IDictionary serializedAdditionalRawData, string internalLogs) : base(type, serializedAdditionalRawData) + { + InternalLogs = internalLogs; + } + + internal InternalRunStepCodeInterpreterLogOutput() + { + } + } +} diff --git a/src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.cs b/src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.cs deleted file mode 100644 index 03dd6009..00000000 --- a/src/Generated/Models/InternalRunStepDetailsToolCallsCodeOutputLogsObject.cs +++ /dev/null @@ -1,29 +0,0 @@ -// - -#nullable disable - -using System; -using System.Collections.Generic; - -namespace OpenAI.Assistants -{ - internal partial class InternalRunStepDetailsToolCallsCodeOutputLogsObject : RunStepCodeInterpreterOutput - { - internal InternalRunStepDetailsToolCallsCodeOutputLogsObject(string internalLogs) - { - Argument.AssertNotNull(internalLogs, nameof(internalLogs)); - - Type = "logs"; - InternalLogs = internalLogs; - } - - internal InternalRunStepDetailsToolCallsCodeOutputLogsObject(string type, IDictionary serializedAdditionalRawData, string internalLogs) : base(type, serializedAdditionalRawData) - { - InternalLogs = internalLogs; - } - - internal InternalRunStepDetailsToolCallsCodeOutputLogsObject() - { - } - } -} diff --git a/src/Generated/Models/MessageCreationOptions.cs b/src/Generated/Models/MessageCreationOptions.cs index fcb012a9..85f6ee95 100644 --- a/src/Generated/Models/MessageCreationOptions.cs +++ b/src/Generated/Models/MessageCreationOptions.cs @@ -20,8 +20,6 @@ internal MessageCreationOptions(MessageRole role, IList content, Metadata = metadata; _serializedAdditionalRawData = serializedAdditionalRawData; } - - public MessageRole Role { get; } public IList Attachments { get; } public IDictionary Metadata { get; } } diff --git a/src/Generated/Models/RunStepCodeInterpreterOutput.Serialization.cs b/src/Generated/Models/RunStepCodeInterpreterOutput.Serialization.cs index 6f04ba49..b5ed2a2f 100644 --- a/src/Generated/Models/RunStepCodeInterpreterOutput.Serialization.cs +++ b/src/Generated/Models/RunStepCodeInterpreterOutput.Serialization.cs @@ -66,7 +66,7 @@ internal static RunStepCodeInterpreterOutput DeserializeRunStepCodeInterpreterOu switch (discriminator.GetString()) { case "image": return InternalRunStepDetailsToolCallsCodeOutputImageObject.DeserializeInternalRunStepDetailsToolCallsCodeOutputImageObject(element, options); - case "logs": return InternalRunStepDetailsToolCallsCodeOutputLogsObject.DeserializeInternalRunStepDetailsToolCallsCodeOutputLogsObject(element, options); + case "logs": return InternalRunStepCodeInterpreterLogOutput.DeserializeInternalRunStepCodeInterpreterLogOutput(element, options); } } return UnknownRunStepDetailsToolCallsCodeObjectCodeInterpreterOutputsObject.DeserializeUnknownRunStepDetailsToolCallsCodeObjectCodeInterpreterOutputsObject(element, options); diff --git a/tests/Assistants/AssistantTests.cs b/tests/Assistants/AssistantTests.cs index 8f8ca41e..8ee92f31 100644 --- a/tests/Assistants/AssistantTests.cs +++ b/tests/Assistants/AssistantTests.cs @@ -96,7 +96,7 @@ public void BasicMessageOperationsWork() AssistantClient client = GetTestClient(); AssistantThread thread = client.CreateThread(); Validate(thread); - ThreadMessage message = client.CreateMessage(thread, ["Hello, world!"]); + ThreadMessage message = client.CreateMessage(thread, MessageRole.User, ["Hello, world!"]); Validate(message); Assert.That(message.CreatedAt, Is.GreaterThan(s_2024)); Assert.That(message.Content?.Count, Is.EqualTo(1)); @@ -106,7 +106,7 @@ public void BasicMessageOperationsWork() Assert.That(deleted, Is.True); _messagesToDelete.Remove(message); - message = client.CreateMessage(thread, ["Goodbye, world!"], new MessageCreationOptions() + message = client.CreateMessage(thread, MessageRole.User, ["Goodbye, world!"], new MessageCreationOptions() { Metadata = { @@ -142,12 +142,13 @@ public void ThreadWithInitialMessagesWorks() { InitialMessages = { - new(["Hello, world!"]), + "Hello, world!", new( - [ - "Can you describe this image for me?", - MessageContent.FromImageUrl(new Uri("https://test.openai.com/image.png")) - ]) + MessageRole.User, + [ + "Can you describe this image for me?", + MessageContent.FromImageUrl(new Uri("https://test.openai.com/image.png")) + ]) { Metadata = { @@ -180,7 +181,7 @@ public void BasicRunOperationsWork() Validate(thread); PageableCollection runs = client.GetRuns(thread); Assert.That(runs.Count, Is.EqualTo(0)); - ThreadMessage message = client.CreateMessage(thread.Id, ["Hello, assistant!"]); + ThreadMessage message = client.CreateMessage(thread.Id, MessageRole.User, ["Hello, assistant!"]); Validate(message); ThreadRun run = client.CreateRun(thread.Id, assistant.Id); Validate(run); @@ -218,16 +219,40 @@ public void BasicRunOperationsWork() public void BasicRunStepFunctionalityWorks() { AssistantClient client = GetTestClient(); - Assistant assistant = client.CreateAssistant("gpt-3.5-turbo", new AssistantCreationOptions() + Assistant assistant = client.CreateAssistant("gpt-4o", new AssistantCreationOptions() { Tools = { new CodeInterpreterToolDefinition() }, - Instructions = "Call the code interpreter tool when asked to visualize mathematical concepts.", + Instructions = "You help the user with mathematical descriptions and visualizations.", }); Validate(assistant); + FileClient fileClient = new(); + OpenAIFileInfo equationFile = fileClient.UploadFile( + BinaryData.FromString(""" + x,y + 2,5 + 7,14, + 8,22 + """).ToStream(), + "text/csv", + FileUploadPurpose.Assistants); + Validate(equationFile); + AssistantThread thread = client.CreateThread(new ThreadCreationOptions() { - InitialMessages = { new(["Please graph the equation y = 3x + 4"]), }, + InitialMessages = + { + "Describe the contents of any available tool resource file." + + " Graph a linear regression and provide the coefficient of correlation." + + " Explain any code executed to evaluate.", + }, + ToolResources = new() + { + CodeInterpreter = new() + { + FileIds = { equationFile.Id }, + } + } }); Validate(thread); @@ -255,6 +280,7 @@ public void BasicRunStepFunctionalityWorks() RunStepDetails details = runSteps.First().Details; Assert.That(details?.CreatedMessageId, Is.Not.Null.And.Not.Empty); + string rawContent = runSteps.GetRawResponse().Content.ToString(); details = runSteps.ElementAt(1).Details; Assert.Multiple(() => { @@ -284,7 +310,7 @@ public void SettingResponseFormatWorks() Assert.That(assistant.ResponseFormat, Is.EqualTo(AssistantResponseFormat.Text)); AssistantThread thread = client.CreateThread(); Validate(thread); - ThreadMessage message = client.CreateMessage(thread, ["Write some JSON for me!"]); + ThreadMessage message = client.CreateMessage(thread, MessageRole.User, ["Write some JSON for me!"]); Validate(message); ThreadRun run = client.CreateRun(thread, assistant, new() { @@ -332,7 +358,7 @@ public void FunctionToolsWork() assistant, new ThreadCreationOptions() { - InitialMessages = { new(["What should I eat on Thursday?"]) }, + InitialMessages = { "What should I eat on Thursday?" }, }, new RunCreationOptions() { @@ -377,7 +403,7 @@ public async Task StreamingRunWorks() AssistantThread thread = await client.CreateThreadAsync(new ThreadCreationOptions() { - InitialMessages = { new(["Hello there, assistant! How are you today?"]), }, + InitialMessages = { "Hello there, assistant! How are you today?", }, }); Validate(thread); @@ -435,7 +461,7 @@ public async Task StreamingToolCall() assistant, new() { - InitialMessages = { new(["What should I wear outside right now?"]), }, + InitialMessages = { "What should I wear outside right now?", }, }); Print(" >>> Starting enumeration ..."); @@ -530,7 +556,7 @@ This file describes the favorite foods of several people. // Create a thread with an override vector store AssistantThread thread = client.CreateThread(new ThreadCreationOptions() { - InitialMessages = { new(["Using the files you have available, what's Filip's favorite food?"]) }, + InitialMessages = { "Using the files you have available, what's Filip's favorite food?" }, ToolResources = new() { FileSearch = new() @@ -676,6 +702,115 @@ public async Task CanPageThroughAssistantCollection() Assert.That(pageCount, Is.GreaterThanOrEqualTo(5)); } + [Test] + public async Task MessagesWithRoles() + { + AssistantClient client = GetTestClient(); + const string userMessageText = "Hello, assistant!"; + const string assistantMessageText = "Hi there, user."; + AssistantThread thread = await client.CreateThreadAsync(new ThreadCreationOptions() + { + InitialMessages = + { + new ThreadInitializationMessage(MessageRole.User, [userMessageText]), + new ThreadInitializationMessage(MessageRole.Assistant, [assistantMessageText]), + } + }); + Validate(thread); + List messages = []; + async Task RefreshMessageListAsync() + { + messages.Clear(); + await foreach (ThreadMessage message in client.GetMessagesAsync(thread)) + { + messages.Add(message); + } + } + await RefreshMessageListAsync(); + Assert.That(messages.Count, Is.EqualTo(2)); + Assert.That(messages[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messages[1].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messages[1].Content[0].Text, Is.EqualTo(userMessageText)); + Assert.That(messages[1].Content[0].Text, Is.EqualTo(userMessageText)); + Assert.That(messages[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messages[0].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messages[0].Content[0].Text, Is.EqualTo(assistantMessageText)); + Assert.That(messages[0].Content[0].Text, Is.EqualTo(assistantMessageText)); + ThreadMessage userMessage = await client.CreateMessageAsync( + thread, + MessageRole.User, + [ + MessageContent.FromText(userMessageText) + ]); + ThreadMessage assistantMessage = await client.CreateMessageAsync( + thread, + MessageRole.Assistant, + [assistantMessageText]); + await RefreshMessageListAsync(); + Assert.That(messages.Count, Is.EqualTo(4)); + Assert.That(messages[3].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messages[3].Role, Is.EqualTo(MessageRole.User)); + Assert.That(messages[3].Content[0].Text, Is.EqualTo(userMessageText)); + Assert.That(messages[3].Content[0].Text, Is.EqualTo(userMessageText)); + Assert.That(messages[2].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messages[2].Role, Is.EqualTo(MessageRole.Assistant)); + Assert.That(messages[2].Content[0].Text, Is.EqualTo(assistantMessageText)); + Assert.That(messages[2].Content[0].Text, Is.EqualTo(assistantMessageText)); + } + + [Test] + public void RunStepDeserialization() + { + BinaryData runStepData = BinaryData.FromString( + """ + { + "id": "step_Ksdfr5ooy26sayKbIQu2d2Vb", + "object": "thread.run.step", + "created_at": 1718906747, + "run_id": "run_vvuLqtPTte9qCnRb7a5MQPgB", + "assistant_id": "asst_UyBYTjqlwhSOdHOEzwwGZM6d", + "thread_id": "thread_lIk2yQzSGHzXrzA4K6N8uPae", + "type": "tool_calls", + "status": "completed", + "cancelled_at": null, + "completed_at": 1718906749, + "expires_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "tool_calls", + "tool_calls": [ + { + "id": "call_DUP8WOybwaxKcMoxtr6cJDw1", + "type": "code_interpreter", + "code_interpreter": { + "input": "# Let's read the content of the uploaded file to understand its content.\r\nfile_path = '/mnt/data/assistant-SvXXKd0VKpGbVq9rBDlvZTn0'\r\nwith open(file_path, 'r') as file:\r\n content = file.read()\r\n\r\n# Output the first few lines of the file to understand its structure and content\r\ncontent[:2000]", + "outputs": [ + { + "type": "logs", + "logs": "'Index,Value\\nIndex #1,1\\nIndex #2,4\\nIndex #3,9\\nIndex #4,16\\nIndex #5,25\\nIndex #6,36\\nIndex #7,49\\nIndex #8,64\\nIndex #9,81\\nIndex #10,100\\nIndex #11,121\\nIndex #12,144\\nIndex #13,169\\nIndex #14,196\\nIndex #15,225\\nIndex #16,256\\nIndex #17,289\\nIndex #18,324\\nIndex #19,361\\nIndex #20,400\\nIndex #21,441\\nIndex #22,484\\nIndex #23,529\\nIndex #24,576\\nIndex #25,625\\nIndex #26,676\\nIndex #27,729\\nIndex #28,784\\nIndex #29,841\\nIndex #30,900\\nIndex #31,961\\nIndex #32,1024\\nIndex #33,1089\\nIndex #34,1156\\nIndex #35,1225\\nIndex #36,1296\\nIndex #37,1369\\nIndex #38,1444\\nIndex #39,1521\\nIndex #40,1600\\nIndex #41,1681\\nIndex #42,1764\\nIndex #43,1849\\nIndex #44,1936\\nIndex #45,2025\\nIndex #46,2116\\nIndex #47,2209\\nIndex #48,2304\\nIndex #49,2401\\nIndex #50,2500\\nIndex #51,2601\\nIndex #52,2704\\nIndex #53,2809\\nIndex #54,2916\\nIndex #55,3025\\nIndex #56,3136\\nIndex #57,3249\\nIndex #58,3364\\nIndex #59,3481\\nIndex #60,3600\\nIndex #61,3721\\nIndex #62,3844\\nIndex #63,3969\\nIndex #64,4096\\nIndex #65,4225\\nIndex #66,4356\\nIndex #67,4489\\nIndex #68,4624\\nIndex #69,4761\\nIndex #70,4900\\nIndex #71,5041\\nIndex #72,5184\\nIndex #73,5329\\nIndex #74,5476\\nIndex #75,5625\\nIndex #76,5776\\nIndex #77,5929\\nIndex #78,6084\\nIndex #79,6241\\nIndex #80,6400\\nIndex #81,6561\\nIndex #82,6724\\nIndex #83,6889\\nIndex #84,7056\\nIndex #85,7225\\nIndex #86,7396\\nIndex #87,7569\\nIndex #88,7744\\nIndex #89,7921\\nIndex #90,8100\\nIndex #91,8281\\nIndex #92,8464\\nIndex #93,8649\\nIndex #94,8836\\nIndex #95,9025\\nIndex #96,9216\\nIndex #97,9409\\nIndex #98,9604\\nIndex #99,9801\\nIndex #100,10000\\nIndex #101,10201\\nIndex #102,10404\\nIndex #103,10609\\nIndex #104,10816\\nIndex #105,11025\\nIndex #106,11236\\nIndex #107,11449\\nIndex #108,11664\\nIndex #109,11881\\nIndex #110,12100\\nIndex #111,12321\\nIndex #112,12544\\nIndex #113,12769\\nIndex #114,12996\\nIndex #115,13225\\nIndex #116,13456\\nIndex #117,13689\\nIndex #118,13924\\nIndex #119,14161\\nIndex #120,14400\\nIndex #121,14641\\nIndex #122,14884\\nIndex #123,15129\\nIndex #124,15376\\nIndex #125,15625\\nIndex #126,15876\\nIndex #127,16129\\nIndex #128,16384\\nIndex #129,16641\\nIndex #130,16900\\nIndex #131,17161\\nIndex #132,'" + } + ] + } + } + ] + }, + "usage": { + "prompt_tokens": 201, + "completion_tokens": 84, + "total_tokens": 285 + } + } + """); + RunStep deserializedRunStep = ModelReaderWriter.Read(runStepData); + Assert.That(deserializedRunStep.Id, Is.Not.Null.And.Not.Empty); + Assert.That(deserializedRunStep.AssistantId, Is.Not.Null.And.Not.Empty); + Assert.That(deserializedRunStep.Details, Is.Not.Null); + Assert.That(deserializedRunStep.Details.ToolCalls, Has.Count.EqualTo(1)); + Assert.That(deserializedRunStep.Details.ToolCalls[0].CodeInterpreterOutputs, Has.Count.EqualTo(1)); + Assert.That(deserializedRunStep.Details.ToolCalls[0].CodeInterpreterOutputs[0].Logs, Is.Not.Null.And.Not.Empty); + } + [TearDown] protected void Cleanup() {