diff --git a/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs new file mode 100644 index 00000000..3428adaf --- /dev/null +++ b/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs @@ -0,0 +1,94 @@ +using NUnit.Framework; +using System.Collections.Generic; +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() + { + new CreateThreadRequest.Message("Test message") + }, + Metadata = new Dictionary + { + ["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); + Assert.AreEqual("test", result.Metadata["text"]); + } + + [Test] + public async Task Test_02_RetrieveThread() + { + Assert.IsNotNull(OpenAIClient.ThreadsEndpoint); + + var createThreadRequest = new CreateThreadRequest + { + Metadata = new Dictionary + { + ["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); + 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 + { + ["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); + 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); + } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/OpenAIClient.cs b/OpenAI-DotNet/OpenAIClient.cs index 501665f2..aae888a0 100644 --- a/OpenAI-DotNet/OpenAIClient.cs +++ b/OpenAI-DotNet/OpenAIClient.cs @@ -15,6 +15,7 @@ using System.Security.Authentication; using System.Text.Json; using System.Text.Json.Serialization; +using OpenAI.Threads; namespace OpenAI { @@ -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) @@ -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) || @@ -192,5 +195,11 @@ private HttpClient SetupClient(HttpClient client = null) /// /// public ModerationsEndpoint ModerationsEndpoint { get; } + + /// + /// Create threads that assistants can interact with. + /// + /// + public ThreadsEndpoint ThreadsEndpoint { get; } } -} +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/CreateThreadRequest.cs b/OpenAI-DotNet/Threads/CreateThreadRequest.cs new file mode 100644 index 00000000..0c7216b1 --- /dev/null +++ b/OpenAI-DotNet/Threads/CreateThreadRequest.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads; + +public class CreateThreadRequest +{ + /// + /// A list of messages to start the thread with. + /// + [JsonPropertyName("messages")] + public List Messages { get; set; } = new(); + + /// + /// 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. + /// + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + + public class Message + { + public Message() { } + + public Message(string content) + { + Content = content; + } + + /// + /// The role of the entity that is creating the message. Currently only user is supported. + /// + [JsonPropertyName("role")] + public string Role { get; set; } = "user"; + + /// + /// The content of the message. + /// + [JsonPropertyName("content")] + public string Content { get; set; } + + /// + /// 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. + /// + [JsonPropertyName("file_ids")] + public string[] FileIds { get; set; } + + /// + /// 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. + /// + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } + } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/Thread.cs b/OpenAI-DotNet/Threads/Thread.cs new file mode 100644 index 00000000..6fc3c296 --- /dev/null +++ b/OpenAI-DotNet/Threads/Thread.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace OpenAI.Threads; + +public class Thread +{ + /// + /// The identifier, which can be referenced in API endpoints. + /// + [JsonPropertyName("id")] + public string Id { get; set; } + + /// + /// The object type, which is always thread. + /// + [JsonPropertyName("object")] + public string Object { get; set; } = "thread"; + + /// + /// The Unix timestamp (in seconds) for when the thread was created. + /// + /// + [JsonPropertyName("created_at")] + public int CreatedAt { get; set; } + + /// + /// 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. + /// + [JsonPropertyName("metadata")] + public Dictionary Metadata { get; set; } +} \ No newline at end of file diff --git a/OpenAI-DotNet/Threads/ThreadsEndpoint.cs b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs new file mode 100644 index 00000000..1bc0ceab --- /dev/null +++ b/OpenAI-DotNet/Threads/ThreadsEndpoint.cs @@ -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"; + + /// + /// Create a thread. + /// + /// + /// + /// A thread object. + public async Task 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(responseAsString, OpenAIClient.JsonSerializationOptions); + + return created; + } + + /// + /// Retrieves a thread. + /// + /// The ID of the thread to retrieve. + /// + /// The thread object matching the specified ID. + public async Task 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(responseAsString, OpenAIClient.JsonSerializationOptions); + + return thread; + } + + /// + /// Modifies a thread. + /// + /// The ID of the thread to modify. Only the metadata can be modified. + /// 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. + /// + /// The modified thread object matching the specified ID. + public async Task ModifyThreadAsync(string threadId, Dictionary 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(responseAsString, OpenAIClient.JsonSerializationOptions); + + return thread; + } + + /// + /// Delete a thread. + /// + /// The ID of the thread to delete. + /// + /// True, if was successfully deleted. + public async Task 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(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; } + } +} \ No newline at end of file