Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Threads Endpoint #167

Merged
merged 3 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions OpenAI-DotNet-Tests/TestFixture_13_Threads.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenAI.Threads;

namespace OpenAI.Tests
{
internal class TestFixture_13_Theads : AbstractTestFixture
{
[Test]
public async Task Test_01_CreateThread()
{
Assert.IsNotNull(OpenAIClient.ThreadsEndpoint);

var request = new CreateThreadRequest
{
Messages = new List<CreateThreadRequest.Message>()
{
new CreateThreadRequest.Message("Test message")
},
Metadata = new Dictionary<string, object>
{
["text"] = "test"
}
};

var result = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(request);

Assert.IsNotNull(result);
Assert.AreEqual("thread", result.Object);

Assert.IsNotNull(result.Metadata);
Assert.Contains("text", result.Metadata.Keys.ToList());
Assert.AreEqual("test", result.Metadata["text"]);
}

[Test]
public async Task Test_02_RetrieveThread()
{
Assert.IsNotNull(OpenAIClient.ThreadsEndpoint);

var createThreadRequest = new CreateThreadRequest
{
Metadata = new Dictionary<string, object>
{
["text"] = "test"
}
};

var created = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(createThreadRequest);

var retrieved = await OpenAIClient.ThreadsEndpoint.RetrieveThreadAsync(created.Id);

Assert.IsNotNull(retrieved);
Assert.AreEqual(created.Id, retrieved.Id);
Assert.IsNotNull(retrieved.Metadata);
Assert.Contains("text", retrieved.Metadata.Keys.ToList());
Assert.AreEqual("test", retrieved.Metadata["text"]);
}

[Test]
public async Task Test_03_ModifyThread()
{
Assert.IsNotNull(OpenAIClient.ThreadsEndpoint);

var created = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(new CreateThreadRequest());

var newMetadata = new Dictionary<string, object>
{
["text"] = "test2"
};

var modified = await OpenAIClient.ThreadsEndpoint.ModifyThreadAsync(created.Id, newMetadata);

Assert.IsNotNull(modified);
Assert.AreEqual(created.Id, modified.Id);
Assert.IsNotNull(modified.Metadata);
Assert.Contains("text", modified.Metadata.Keys.ToList());
Assert.AreEqual("test2", modified.Metadata["text"]);
}

[Test]
public async Task Test_04_DeleteThread()
{
Assert.IsNotNull(OpenAIClient.ThreadsEndpoint);

var created = await OpenAIClient.ThreadsEndpoint.CreateThreadAsync(new CreateThreadRequest());

var isDeleted = await OpenAIClient.ThreadsEndpoint.DeleteThreadAsync(created.Id);

Assert.IsTrue(isDeleted);
}
}
}
11 changes: 10 additions & 1 deletion OpenAI-DotNet/OpenAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Security.Authentication;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenAI.Threads;

namespace OpenAI
{
Expand Down Expand Up @@ -59,6 +60,7 @@ public OpenAIClient(OpenAIAuthentication openAIAuthentication = null, OpenAIClie
FilesEndpoint = new FilesEndpoint(this);
FineTuningEndpoint = new FineTuningEndpoint(this);
ModerationsEndpoint = new ModerationsEndpoint(this);
ThreadsEndpoint = new ThreadsEndpoint(this);
}

private HttpClient SetupClient(HttpClient client = null)
Expand All @@ -68,6 +70,7 @@ private HttpClient SetupClient(HttpClient client = null)
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
});
client.DefaultRequestHeaders.Add("User-Agent", "OpenAI-DotNet");
client.DefaultRequestHeaders.Add("OpenAI-Beta", "assistants=v1");

if (!OpenAIClientSettings.BaseRequestUrlFormat.Contains(OpenAIClientSettings.AzureOpenAIDomain) &&
(string.IsNullOrWhiteSpace(OpenAIAuthentication.ApiKey) ||
Expand Down Expand Up @@ -192,5 +195,11 @@ private HttpClient SetupClient(HttpClient client = null)
/// <see href="https://platform.openai.com/docs/api-reference/moderations"/>
/// </summary>
public ModerationsEndpoint ModerationsEndpoint { get; }

/// <summary>
/// Create threads that assistants can interact with.
/// <see href="https://platform.openai.com/docs/api-reference/threads"/>
/// </summary>
public ThreadsEndpoint ThreadsEndpoint { get; }
}
}
}
60 changes: 60 additions & 0 deletions OpenAI-DotNet/Threads/CreateThreadRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using OpenAI.Chat;

namespace OpenAI.Threads;

public sealed class CreateThreadRequest
{
/// <summary>
/// A list of messages to start the thread with.
/// </summary>
[JsonPropertyName("messages")]
public List<Message> Messages { get; set; } = new();

/// <summary>
/// Set of 16 key-value pairs that can be attached to an object.
/// This can be useful for storing additional information about the object in a structured format.
/// Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, object> Metadata { get; set; }

public class Message
{
public Message() { }

public Message(string content)
{
Role = Role.User;
Content = content;
}

/// <summary>
/// The role of the entity that is creating the message. Currently only user is supported.
/// </summary>
[JsonPropertyName("role")]
public Role Role { get; set; }

/// <summary>
/// The content of the message.
/// </summary>
[JsonPropertyName("content")]
public string Content { get; set; }

/// <summary>
/// A list of File IDs that the message should use. There can be a maximum of 10 files attached to a message.
/// Useful for tools like retrieval and code_interpreter that can access and use files.
/// </summary>
[JsonPropertyName("file_ids")]
public string[] FileIds { get; set; }

/// <summary>
/// Set of 16 key-value pairs that can be attached to an object.
/// This can be useful for storing additional information about the object in a structured format.
/// Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, string> Metadata { get; set; }
}
}
34 changes: 34 additions & 0 deletions OpenAI-DotNet/Threads/Thread.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace OpenAI.Threads;

public sealed class Thread
{
/// <summary>
/// The identifier, which can be referenced in API endpoints.
/// </summary>
[JsonPropertyName("id")]
public string Id { get; set; }

/// <summary>
/// The object type, which is always thread.
/// </summary>
[JsonPropertyName("object")]
public string Object { get; set; } = "thread";

/// <summary>
/// The Unix timestamp (in seconds) for when the thread was created.
/// </summary>
/// <returns></returns>
[JsonPropertyName("created_at")]
public int CreatedAt { get; set; }

/// <summary>
/// Set of 16 key-value pairs that can be attached to an object.
/// This can be useful for storing additional information about the object in a structured format.
/// Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
/// </summary>
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, string> Metadata { get; set; }
}
103 changes: 103 additions & 0 deletions OpenAI-DotNet/Threads/ThreadsEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using OpenAI.Extensions;

namespace OpenAI.Threads;

public class ThreadsEndpoint : BaseEndPoint
{
public ThreadsEndpoint(OpenAIClient api) : base(api)
{
}

protected override string Root => "/threads";

/// <summary>
/// Create a thread.
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns>A thread object.</returns>
public async Task<Thread> CreateThreadAsync(CreateThreadRequest request,
CancellationToken cancellationToken = default)
{
var jsonContent = JsonSerializer.Serialize(request, OpenAIClient.JsonSerializationOptions)
.ToJsonStringContent(EnableDebug);
var response = await Api.Client.PostAsync(GetUrl(), jsonContent, cancellationToken).ConfigureAwait(false);
var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
var created = JsonSerializer.Deserialize<Thread>(responseAsString, OpenAIClient.JsonSerializationOptions);

return created;
}

/// <summary>
/// Retrieves a thread.
/// </summary>
/// <param name="threadId">The ID of the thread to retrieve.</param>
/// <param name="cancellationToken"></param>
/// <returns>The thread object matching the specified ID.</returns>
public async Task<Thread> RetrieveThreadAsync(string threadId, CancellationToken cancellationToken = default)
{
var response = await Api.Client.GetAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
var thread = JsonSerializer.Deserialize<Thread>(responseAsString, OpenAIClient.JsonSerializationOptions);

return thread;
}

/// <summary>
/// Modifies a thread.
/// </summary>
/// <param name="threadId">The ID of the thread to modify. Only the metadata can be modified.</param>
/// <param name="metadata">Set of 16 key-value pairs that can be attached to an object.
/// This can be useful for storing additional information about the object in a structured format.
/// Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.</param>
/// <param name="cancellationToken"></param>
/// <returns>The modified thread object matching the specified ID.</returns>
public async Task<Thread> ModifyThreadAsync(string threadId, Dictionary<string, object> metadata,
CancellationToken cancellationToken = default)
{
var jsonContent = JsonSerializer.Serialize(new { metadata = metadata }, OpenAIClient.JsonSerializationOptions)
.ToJsonStringContent(EnableDebug);
var response = await Api.Client.PostAsync(GetUrl($"/{threadId}"), jsonContent, cancellationToken)
.ConfigureAwait(false);
var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
var thread = JsonSerializer.Deserialize<Thread>(responseAsString, OpenAIClient.JsonSerializationOptions);

return thread;
}

/// <summary>
/// Delete a thread.
/// </summary>
/// <param name="threadId">The ID of the thread to delete.</param>
/// <param name="cancellationToken"></param>
/// <returns>True, if was successfully deleted.</returns>
public async Task<bool> DeleteThreadAsync(string threadId, CancellationToken cancellationToken = default)
{
var response = await Api.Client.DeleteAsync(GetUrl($"/{threadId}"), cancellationToken).ConfigureAwait(false);
var responseAsString = await response.ReadAsStringAsync(EnableDebug, cancellationToken).ConfigureAwait(false);
var status =
JsonSerializer.Deserialize<DeletionStatus>(responseAsString, OpenAIClient.JsonSerializationOptions);

return status.Deleted;
}

private sealed class DeletionStatus
{
[JsonInclude]
[JsonPropertyName("id")]
public string Id { get; private set; }

[JsonInclude]
[JsonPropertyName("object")]
public string Object { get; private set; }

[JsonInclude]
[JsonPropertyName("deleted")]
public bool Deleted { get; private set; }
}
}