From bcd1f7a6c63c9058e523d82de7f8d76ebdba39c6 Mon Sep 17 00:00:00 2001 From: Evgenii Khoroshev Date: Wed, 15 Nov 2023 10:29:11 +0300 Subject: [PATCH 1/2] threads --- OpenAI-DotNet-Tests/TestFixture_13_Threads.cs | 94 ++++++++++++++++ OpenAI-DotNet/OpenAIClient.cs | 11 +- OpenAI-DotNet/Threads/CreateThreadRequest.cs | 58 ++++++++++ OpenAI-DotNet/Threads/Thread.cs | 34 ++++++ OpenAI-DotNet/Threads/ThreadsEndpoint.cs | 103 ++++++++++++++++++ 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 OpenAI-DotNet-Tests/TestFixture_13_Threads.cs create mode 100644 OpenAI-DotNet/Threads/CreateThreadRequest.cs create mode 100644 OpenAI-DotNet/Threads/Thread.cs create mode 100644 OpenAI-DotNet/Threads/ThreadsEndpoint.cs 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 From 7c8c8451cd7fbc4b701db264accd8d1f4fde12a2 Mon Sep 17 00:00:00 2001 From: Evgenii Khoroshev Date: Wed, 15 Nov 2023 18:39:07 +0300 Subject: [PATCH 2/2] sealed + readonly --- OpenAI-DotNet-Tests/TestFixture_13_Threads.cs | 7 ++++--- OpenAI-DotNet/Threads/CreateThreadRequest.cs | 6 ++++-- OpenAI-DotNet/Threads/Thread.cs | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs b/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs index 3428adaf..2403e301 100644 --- a/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs +++ b/OpenAI-DotNet-Tests/TestFixture_13_Threads.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using OpenAI.Threads; @@ -30,7 +31,7 @@ public async Task Test_01_CreateThread() Assert.AreEqual("thread", result.Object); Assert.IsNotNull(result.Metadata); - Assert.Contains("text", result.Metadata.Keys); + Assert.Contains("text", result.Metadata.Keys.ToList()); Assert.AreEqual("test", result.Metadata["text"]); } @@ -54,7 +55,7 @@ public async Task Test_02_RetrieveThread() Assert.IsNotNull(retrieved); Assert.AreEqual(created.Id, retrieved.Id); Assert.IsNotNull(retrieved.Metadata); - Assert.Contains("text", retrieved.Metadata.Keys); + Assert.Contains("text", retrieved.Metadata.Keys.ToList()); Assert.AreEqual("test", retrieved.Metadata["text"]); } @@ -75,7 +76,7 @@ public async Task Test_03_ModifyThread() Assert.IsNotNull(modified); Assert.AreEqual(created.Id, modified.Id); Assert.IsNotNull(modified.Metadata); - Assert.Contains("text", modified.Metadata.Keys); + Assert.Contains("text", modified.Metadata.Keys.ToList()); Assert.AreEqual("test2", modified.Metadata["text"]); } diff --git a/OpenAI-DotNet/Threads/CreateThreadRequest.cs b/OpenAI-DotNet/Threads/CreateThreadRequest.cs index 0c7216b1..c90a1a26 100644 --- a/OpenAI-DotNet/Threads/CreateThreadRequest.cs +++ b/OpenAI-DotNet/Threads/CreateThreadRequest.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using OpenAI.Chat; namespace OpenAI.Threads; -public class CreateThreadRequest +public sealed class CreateThreadRequest { /// /// A list of messages to start the thread with. @@ -25,6 +26,7 @@ public Message() { } public Message(string content) { + Role = Role.User; Content = content; } @@ -32,7 +34,7 @@ public Message(string content) /// The role of the entity that is creating the message. Currently only user is supported. /// [JsonPropertyName("role")] - public string Role { get; set; } = "user"; + public Role Role { get; set; } /// /// The content of the message. diff --git a/OpenAI-DotNet/Threads/Thread.cs b/OpenAI-DotNet/Threads/Thread.cs index 6fc3c296..50ba1201 100644 --- a/OpenAI-DotNet/Threads/Thread.cs +++ b/OpenAI-DotNet/Threads/Thread.cs @@ -3,7 +3,7 @@ namespace OpenAI.Threads; -public class Thread +public sealed class Thread { /// /// The identifier, which can be referenced in API endpoints. @@ -30,5 +30,5 @@ public class Thread /// 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 IReadOnlyDictionary Metadata { get; set; } } \ No newline at end of file