Skip to content

Commit

Permalink
threads
Browse files Browse the repository at this point in the history
  • Loading branch information
Evgenii Khoroshev committed Nov 15, 2023
1 parent 5de096c commit bcd1f7a
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 1 deletion.
94 changes: 94 additions & 0 deletions OpenAI-DotNet-Tests/TestFixture_13_Threads.cs
Original file line number Diff line number Diff line change
@@ -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<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);
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);
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);
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; }
}
}
}
58 changes: 58 additions & 0 deletions OpenAI-DotNet/Threads/CreateThreadRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace OpenAI.Threads;

public 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)
{
Content = content;
}

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

/// <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 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 Dictionary<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; }
}
}

0 comments on commit bcd1f7a

Please sign in to comment.