Skip to content

Commit

Permalink
Fix Assistants issues with Message role, Code logs (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
trrwilson authored Jun 21, 2024
1 parent 9ee7dff commit d665b61
Show file tree
Hide file tree
Showing 19 changed files with 242 additions and 94 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>()
{
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);
Expand Down
1 change: 1 addition & 0 deletions examples/Assistants/Example02b_FunctionCallingStreaming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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?"
]);
Expand Down
1 change: 1 addition & 0 deletions examples/Assistants/Example05_AssistantsWithVision.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions examples/Assistants/Example05_AssistantsWithVisionAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions src/Custom/Assistants/AssistantClient.Convenience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,27 +99,31 @@ public virtual ClientResult<bool> DeleteThread(AssistantThread thread)
/// Creates a new <see cref="ThreadMessage"/> on an existing <see cref="AssistantThread"/>.
/// </summary>
/// <param name="thread"> The thread to associate the new message with. </param>
/// <param name="role"> The role to associate with the new message. </param>
/// <param name="content"> The collection of <see cref="MessageContent"/> items for the message. </param>
/// <param name="options"> Additional options to apply to the new message. </param>
/// <returns> A new <see cref="ThreadMessage"/>. </returns>
public virtual Task<ClientResult<ThreadMessage>> CreateMessageAsync(
AssistantThread thread,
MessageRole role,
IEnumerable<MessageContent> content,
MessageCreationOptions options = null)
=> CreateMessageAsync(thread?.Id, content, options);
=> CreateMessageAsync(thread?.Id, role, content, options);

/// <summary>
/// Creates a new <see cref="ThreadMessage"/> on an existing <see cref="AssistantThread"/>.
/// </summary>
/// <param name="thread"> The thread to associate the new message with. </param>
/// <param name="role"> The role to associate with the new message. </param>
/// <param name="content"> The collection of <see cref="MessageContent"/> items for the message. </param>
/// <param name="options"> Additional options to apply to the new message. </param>
/// <returns> A new <see cref="ThreadMessage"/>. </returns>
public virtual ClientResult<ThreadMessage> CreateMessage(
AssistantThread thread,
MessageRole role,
IEnumerable<MessageContent> content,
MessageCreationOptions options = null)
=> CreateMessage(thread?.Id, content, options);
=> CreateMessage(thread?.Id, role, content, options);

/// <summary>
/// Returns a collection of <see cref="ThreadMessage"/> instances from an existing <see cref="AssistantThread"/>.
Expand Down
6 changes: 6 additions & 0 deletions src/Custom/Assistants/AssistantClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,18 +280,21 @@ public virtual ClientResult<bool> DeleteThread(string threadId, CancellationToke
/// Creates a new <see cref="ThreadMessage"/> on an existing <see cref="AssistantThread"/>.
/// </summary>
/// <param name="threadId"> The ID of the thread to associate the new message with. </param>
/// <param name="role"> The role to associate with the new message. </param>
/// <param name="content"> The collection of <see cref="MessageContent"/> items for the message. </param>
/// <param name="options"> Additional options to apply to the new message. </param>
/// <param name="cancellationToken">A token that can be used to cancel this method call.</param>
/// <returns> A new <see cref="ThreadMessage"/>. </returns>
public virtual async Task<ClientResult<ThreadMessage>> CreateMessageAsync(
string threadId,
MessageRole role,
IEnumerable<MessageContent> 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)
{
Expand All @@ -307,18 +310,21 @@ public virtual async Task<ClientResult<ThreadMessage>> CreateMessageAsync(
/// Creates a new <see cref="ThreadMessage"/> on an existing <see cref="AssistantThread"/>.
/// </summary>
/// <param name="threadId"> The ID of the thread to associate the new message with. </param>
/// <param name="role"> The role to associate with the new message. </param>
/// <param name="content"> The collection of <see cref="MessageContent"/> items for the message. </param>
/// <param name="options"> Additional options to apply to the new message. </param>
/// <param name="cancellationToken">A token that can be used to cancel this method call.</param>
/// <returns> A new <see cref="ThreadMessage"/>. </returns>
public virtual ClientResult<ThreadMessage> CreateMessage(
string threadId,
MessageRole role,
IEnumerable<MessageContent> 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)
{
Expand Down
1 change: 0 additions & 1 deletion src/Custom/Assistants/AssistantCreationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,5 @@ public AssistantCreationOptions()
{
Metadata = new ChangeTrackingDictionary<string, string>();
Tools = new ChangeTrackingList<ToolDefinition>();
ToolResources = new();
}
}
7 changes: 0 additions & 7 deletions src/Custom/Assistants/Internal/GeneratorStubs.Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace OpenAI.Assistants
{
/// <summary> Text output from the Code Interpreter tool call as part of a run step. </summary>
[CodeGenModel("RunStepDetailsToolCallsCodeOutputLogsObject")]
internal partial class InternalRunStepCodeInterpreterLogOutput : RunStepCodeInterpreterOutput
{
/// <summary> The text output from the Code Interpreter tool call. </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Custom/Assistants/MessageCreationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion src/Custom/Assistants/RunStepCodeInterpreterOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public abstract partial class RunStepCodeInterpreterOutput
/// <inheritdoc cref="InternalRunStepDetailsToolCallsCodeOutputImageObject.FileId"/>
public string ImageFileId => AsInternalImage?.FileId;
/// <inheritdoc cref="InternalRunStepCodeInterpreterLogOutput.Logs"/>
public string Logs => AsInternalLogs?.Logs;
public string Logs => AsInternalLogs?.InternalLogs;

private InternalRunStepDetailsToolCallsCodeOutputImageObject AsInternalImage => this as InternalRunStepDetailsToolCallsCodeOutputImageObject;
private InternalRunStepCodeInterpreterLogOutput AsInternalLogs => this as InternalRunStepCodeInterpreterLogOutput;
Expand Down
15 changes: 9 additions & 6 deletions src/Custom/Assistants/ThreadInitializationMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@ public partial class ThreadInitializationMessage : MessageCreationOptions
/// <param name="content">
/// The content items that should be included in the message, added to the thread being created.
/// </param>
public ThreadInitializationMessage(IEnumerable<MessageContent> content) : base(content)
{ }
public ThreadInitializationMessage(MessageRole role, IEnumerable<MessageContent> content) : base(content)
{
Role = role;
}

internal ThreadInitializationMessage(MessageCreationOptions baseOptions)
: base(baseOptions.Role, baseOptions.Content, baseOptions.Attachments, baseOptions.Metadata, null)
{ }

/// <summary>
/// Implicitly creates a new instance of <see cref="ThreadInitializationMessage"/> from a single item of plain text
/// content.
/// content, assuming the role of <see cref="MessageRole.User"/>.
/// </summary>
/// <remarks>
/// Using a <see cref="string"/> in the position of a <see cref="ThreadInitializationMessage"/> is equivalent to
/// using the <see cref="ThreadInitializationMessage(IEnumerable{MessageContent})"/> constructor with a single
/// <see cref="MessageContent.FromText(string)"/> content instance.
/// using the <see cref="ThreadInitializationMessage(MessageRole,IEnumerable{MessageContent})"/> constructor with
/// <see cref="MessageRole.User"/> and a single <see cref="MessageContent.FromText(string)"/> content instance.
/// </remarks>
public static implicit operator ThreadInitializationMessage(string initializationMessage) => new([initializationMessage]);
public static implicit operator ThreadInitializationMessage(string initializationMessage)
=> new(MessageRole.User, [initializationMessage]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

namespace OpenAI.Assistants
{
internal partial class InternalRunStepDetailsToolCallsCodeOutputLogsObject : IJsonModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>
internal partial class InternalRunStepCodeInterpreterLogOutput : IJsonModel<InternalRunStepCodeInterpreterLogOutput>
{
void IJsonModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
void IJsonModel<InternalRunStepCodeInterpreterLogOutput>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
{
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>)this).GetFormatFromOptions(options) : options.Format;
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepCodeInterpreterLogOutput>)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();
Expand All @@ -43,19 +43,19 @@ void IJsonModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.Write(Utf8J
writer.WriteEndObject();
}

InternalRunStepDetailsToolCallsCodeOutputLogsObject IJsonModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
InternalRunStepCodeInterpreterLogOutput IJsonModel<InternalRunStepCodeInterpreterLogOutput>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
{
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>)this).GetFormatFromOptions(options) : options.Format;
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepCodeInterpreterLogOutput>)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;

Expand Down Expand Up @@ -85,44 +85,44 @@ internal static InternalRunStepDetailsToolCallsCodeOutputLogsObject DeserializeI
}
}
serializedAdditionalRawData = rawDataDictionary;
return new InternalRunStepDetailsToolCallsCodeOutputLogsObject(type, serializedAdditionalRawData, logs);
return new InternalRunStepCodeInterpreterLogOutput(type, serializedAdditionalRawData, logs);
}

BinaryData IPersistableModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.Write(ModelReaderWriterOptions options)
BinaryData IPersistableModel<InternalRunStepCodeInterpreterLogOutput>.Write(ModelReaderWriterOptions options)
{
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>)this).GetFormatFromOptions(options) : options.Format;
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepCodeInterpreterLogOutput>)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<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.Create(BinaryData data, ModelReaderWriterOptions options)
InternalRunStepCodeInterpreterLogOutput IPersistableModel<InternalRunStepCodeInterpreterLogOutput>.Create(BinaryData data, ModelReaderWriterOptions options)
{
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepDetailsToolCallsCodeOutputLogsObject>)this).GetFormatFromOptions(options) : options.Format;
var format = options.Format == "W" ? ((IPersistableModel<InternalRunStepCodeInterpreterLogOutput>)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<InternalRunStepDetailsToolCallsCodeOutputLogsObject>.GetFormatFromOptions(ModelReaderWriterOptions options) => "J";
string IPersistableModel<InternalRunStepCodeInterpreterLogOutput>.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()
Expand Down
29 changes: 29 additions & 0 deletions src/Generated/Models/InternalRunStepCodeInterpreterLogOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <auto-generated/>

#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<string, BinaryData> serializedAdditionalRawData, string internalLogs) : base(type, serializedAdditionalRawData)
{
InternalLogs = internalLogs;
}

internal InternalRunStepCodeInterpreterLogOutput()
{
}
}
}
Loading

0 comments on commit d665b61

Please sign in to comment.