Skip to content

Commit

Permalink
Add Chat to dependency injection; Add Clear On Disposal feature; Rele…
Browse files Browse the repository at this point in the history
…ase 2.1.0
  • Loading branch information
rodion-m committed Apr 18, 2023
1 parent 930dcd4 commit a035c2b
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenAI.ChatGpt.AspNetCore.Models;

namespace OpenAI.ChatGpt.AspNetCore.Extensions;
Expand All @@ -10,6 +11,7 @@ public static class ServiceCollectionExtensions

public static IServiceCollection AddChatGptInMemoryIntegration(
this IServiceCollection services,
bool injectInMemoryChat = true,
string credentialsConfigSectionPath = CredentialsConfigSectionPathDefault,
string completionsConfigSectionPath = CompletionsConfigSectionPathDefault)
{
Expand All @@ -26,8 +28,35 @@ public static IServiceCollection AddChatGptInMemoryIntegration(
}
services.AddChatGptIntegrationCore(credentialsConfigSectionPath, completionsConfigSectionPath);
services.AddSingleton<IChatHistoryStorage, InMemoryChatHistoryStorage>();
if(injectInMemoryChat)
{
services.AddScoped<Chat>(CreateChatGptChat);
}
return services;
}

private static Chat CreateChatGptChat(IServiceProvider provider)
{
ArgumentNullException.ThrowIfNull(provider);
var userId = Guid.Empty.ToString();
var storage = provider.GetRequiredService<IChatHistoryStorage>();
if(storage is not InMemoryChatHistoryStorage)
{
throw new InvalidOperationException(
$"Chat injection is supported only with {nameof(InMemoryChatHistoryStorage)} " +
$"and is not supported for {storage.GetType().FullName}");
}
/*
* .GetAwaiter().GetResult() are safe here because we use sync in memory storage
*/
var chatGpt = provider.GetRequiredService<ChatGPTFactory>()
.Create(userId)
.GetAwaiter()
.GetResult();
var chat = chatGpt.StartNewTopic(clearOnDisposal: true).GetAwaiter().GetResult();
return chat;
}

public static IServiceCollection AddChatGptIntegrationCore(
this IServiceCollection services,
string credentialsConfigSectionPath = CredentialsConfigSectionPathDefault,
Expand Down
4 changes: 4 additions & 0 deletions OpenAI.ChatGpt.AspNetCore/Models/ChatGptCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ namespace OpenAI.ChatGpt.AspNetCore.Models;

public class ChatGptCredentials
{
/// <summary>
/// OpenAI API key. Can be issued here: https://platform.openai.com/account/api-keys
/// </summary>
[Required]
public string ApiKey { get; set; }

[Url]
public string ApiHost { get; set; } = "https://api.openai.com/v1/";

public AuthenticationHeaderValue GetAuthHeader()
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.ChatGpt.AspNetCore/OpenAI.ChatGpt.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PackageId>OpenAI.ChatGPT.AspNetCore</PackageId>
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI ChatGPT integration for .NET with DI</Product>
<Version>2.0.3</Version>
<Version>2.1.0</Version>
<Description>OpenAI Chat Completions API (ChatGPT) integration with easy DI supporting (Microsoft.Extensions.DependencyInjection). It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ public async Task<bool> DeleteMessage(
return deleted;
}

/// <inheritdoc/>
public Task<bool> ClearMessages(string userId, Guid topicId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
_cache.Remove(GetMessagesKey(topicId));
return _chatHistoryStorage.ClearMessages(userId, topicId, cancellationToken);
}

/// <inheritdoc/>
public async Task<Topic?> GetMostRecentTopicOrNull(string userId, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -196,6 +204,14 @@ public async Task<bool> DeleteTopic(string userId, Guid topicId, CancellationTok
return deleted;
}

/// <inheritdoc/>
public Task<bool> ClearTopics(string userId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
_cache.Remove(GetUserTopicsKey(userId));
return _chatHistoryStorage.ClearTopics(userId, cancellationToken);
}

/// <inheritdoc/>
public Task EnsureStorageCreated(CancellationToken cancellationToken)
=> _chatHistoryStorage.EnsureStorageCreated(cancellationToken);
Expand Down
25 changes: 25 additions & 0 deletions OpenAI.ChatGpt.EntityFrameworkCore/EfChatHistoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ public async Task<bool> DeleteTopic(
return true;
}

/// <inheritdoc/>
public async Task<bool> ClearTopics(string userId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
var topics = await _dbContext.Topics.Where(it => it.UserId == userId).ToListAsync(cancellationToken);
if (topics.Count == 0) return false;
_dbContext.Topics.RemoveRange(topics);
await _dbContext.SaveChangesAsync(cancellationToken);
return true;
}

/// <inheritdoc />
public async Task EditMessage(
string userId,
Expand Down Expand Up @@ -129,6 +140,20 @@ public async Task<bool> DeleteMessage(
return true;
}

/// <inheritdoc/>
public async Task<bool> ClearMessages(
string userId, Guid topicId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
var messages = await _dbContext.Messages
.Where(it => it.TopicId == topicId && it.UserId == userId)
.ToListAsync(cancellationToken);
if (messages.Count == 0) return false;
_dbContext.Messages.RemoveRange(messages);
await _dbContext.SaveChangesAsync(cancellationToken);
return true;
}

/// <inheritdoc />
public Task EnsureStorageCreated(CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageId>OpenAI.ChatGPT.EntityFrameworkCore</PackageId>
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI ChatGPT integration for .NET with EF Core storage</Product>
<Version>2.0.3</Version>
<Version>2.1.0</Version>
<Description>OpenAI Chat Completions API (ChatGPT) integration with DI and EF Core supporting. It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
Expand Down
32 changes: 25 additions & 7 deletions OpenAI.ChatGpt/Chat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,22 @@
namespace OpenAI.ChatGpt;

/// <summary>
/// This is a class that is used for communication between a user and the assistant (ChatGPT).
/// Used for communication between a user and the assistant (ChatGPT).
/// </summary>
/// <remarks>Not thread-safe. Use one instance per user.</remarks>
public class Chat : IDisposable
public class Chat : IDisposable, IAsyncDisposable
{
public Topic Topic { get; }
public string UserId { get; }
public Guid ChatId => Topic.Id;
public Guid TopicId => Topic.Id;
public bool IsWriting { get; private set; }
public bool IsCancelled => _cts?.IsCancellationRequested ?? false;

private readonly IChatHistoryStorage _chatHistoryStorage;
private readonly ITimeProvider _clock;
private readonly OpenAiClient _client;
private bool _isNew;
private readonly bool _clearOnDisposal;
private CancellationTokenSource? _cts;

internal Chat(
Expand All @@ -31,14 +32,16 @@ internal Chat(
OpenAiClient client,
string userId,
Topic topic,
bool isNew)
bool isNew,
bool clearOnDisposal)
{
_chatHistoryStorage = chatHistoryStorage ?? throw new ArgumentNullException(nameof(chatHistoryStorage));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_client = client ?? throw new ArgumentNullException(nameof(client));
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
Topic = topic ?? throw new ArgumentNullException(nameof(topic));
_isNew = isNew;
_clearOnDisposal = clearOnDisposal;
}

public Task<string> GetNextMessageResponse(
Expand Down Expand Up @@ -69,7 +72,7 @@ private async Task<string> GetNextMessageResponse(
);

await _chatHistoryStorage.SaveMessages(
UserId, ChatId, message, response, _clock.GetCurrentTime(), _cts.Token);
UserId, TopicId, message, response, _clock.GetCurrentTime(), _cts.Token);
IsWriting = false;
_isNew = false;

Expand Down Expand Up @@ -118,15 +121,15 @@ private async IAsyncEnumerable<string> StreamNextMessageResponse(
yield break;

await _chatHistoryStorage.SaveMessages(
UserId, ChatId, message, sb.ToString(), _clock.GetCurrentTime(), cancellationToken);
UserId, TopicId, message, sb.ToString(), _clock.GetCurrentTime(), cancellationToken);
IsWriting = false;
_isNew = false;
}

private async Task<IEnumerable<ChatCompletionMessage>> LoadHistory(CancellationToken cancellationToken)
{
if (_isNew) return Enumerable.Empty<ChatCompletionMessage>();
return await _chatHistoryStorage.GetMessages(UserId, ChatId, cancellationToken);
return await _chatHistoryStorage.GetMessages(UserId, TopicId, cancellationToken);
}

public void Stop()
Expand All @@ -137,5 +140,20 @@ public void Stop()
public void Dispose()
{
_cts?.Dispose();
if (_clearOnDisposal)
{
// TODO: log warning about sync disposal
_chatHistoryStorage.DeleteTopic(UserId, TopicId, default)
.GetAwaiter().GetResult();
}
}

public async ValueTask DisposeAsync()
{
_cts?.Dispose();
if (_clearOnDisposal)
{
await _chatHistoryStorage.DeleteTopic(UserId, TopicId, default);
}
}
}
9 changes: 5 additions & 4 deletions OpenAI.ChatGpt/ChatGPT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public async Task<Chat> StartNewTopic(
string? name = null,
ChatCompletionsConfig? config = null,
UserOrSystemMessage? initialDialog = null,
bool clearOnDisposal = false,
CancellationToken cancellationToken = default)
{
config = ChatCompletionsConfig.CombineOrDefault(_config, config);
Expand All @@ -99,7 +100,7 @@ public async Task<Chat> StartNewTopic(
await _chatHistoryStorage.SaveMessages(_userId, topic.Id, messages, cancellationToken);
}

_currentChat = CreateChat(topic, initialDialog is null);
_currentChat = CreateChat(topic, initialDialog is null, clearOnDisposal: clearOnDisposal);
return _currentChat;
}

Expand All @@ -124,14 +125,14 @@ public async Task<Chat> SetTopic(Guid topicId, CancellationToken cancellationTok
private Task<Chat> SetTopic(Topic topic, CancellationToken cancellationToken = default)
{
if (topic == null) throw new ArgumentNullException(nameof(topic));
_currentChat = CreateChat(topic, false);
_currentChat = CreateChat(topic, false, false);
return Task.FromResult(_currentChat);
}

private Chat CreateChat(Topic topic, bool isNew)
private Chat CreateChat(Topic topic, bool isNew, bool clearOnDisposal)
{
if (topic == null) throw new ArgumentNullException(nameof(topic));
return new Chat(_chatHistoryStorage, _clock, _client, _userId, topic, isNew);
return new Chat(_chatHistoryStorage, _clock, _client, _userId, topic, isNew, clearOnDisposal);
}

public async Task<IReadOnlyList<Topic>> GetTopics(CancellationToken cancellationToken = default)
Expand Down
34 changes: 34 additions & 0 deletions OpenAI.ChatGpt/InMemoryChatHistoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ public Task<bool> DeleteTopic(string userId, Guid topicId, CancellationToken can
return Task.FromResult(userChats.Remove(topicId));
}

/// <inheritdoc/>
public Task<bool> ClearTopics(string userId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
cancellationToken.ThrowIfCancellationRequested();
if (!_users.TryGetValue(userId, out var topics))
{
return Task.FromResult(false);
}

topics.Clear();
return Task.FromResult(true);
}

/// <inheritdoc/>
public async Task EditMessage(
string userId,
Expand Down Expand Up @@ -141,6 +155,25 @@ public Task<bool> DeleteMessage(
return Task.FromResult(false);
}

/// <inheritdoc/>
public Task<bool> ClearMessages(string userId, Guid topicId, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userId);
cancellationToken.ThrowIfCancellationRequested();
if (!_messages.TryGetValue(userId, out var userMessages))
{
return Task.FromResult(false);
}

if (userMessages.TryGetValue(topicId, out var chatMessages))
{
chatMessages.Clear();
return Task.FromResult(true);
}

return Task.FromResult(false);
}

/// <inheritdoc/>
public Task<IEnumerable<Topic>> GetTopics(string userId, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -193,4 +226,5 @@ public Task EnsureStorageCreated(CancellationToken cancellationToken)
cancellationToken.ThrowIfCancellationRequested();
return Task.CompletedTask;
}

}
22 changes: 14 additions & 8 deletions OpenAI.ChatGpt/Interfaces/IMessageStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ CancellationToken cancellationToken
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of messages.</returns>
Task<IEnumerable<PersistentChatMessage>> GetMessages(
string userId,
Guid topicId,
CancellationToken cancellationToken
);
string userId, Guid topicId, CancellationToken cancellationToken);

/// <summary>
/// Edits the content of a message.
Expand All @@ -42,8 +39,8 @@ CancellationToken cancellationToken
/// <param name="newMessage">The new message content.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task EditMessage(string userId, Guid topicId, Guid messageId, string newMessage,
CancellationToken cancellationToken);
Task EditMessage(
string userId, Guid topicId, Guid messageId, string newMessage, CancellationToken cancellationToken);

/// <summary>
/// Deletes a message associated with a user and a topic.
Expand All @@ -53,8 +50,17 @@ Task EditMessage(string userId, Guid topicId, Guid messageId, string newMessage,
/// <param name="messageId">The message ID.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean value indicating the success of the deletion.</returns>
Task<bool> DeleteMessage(string userId, Guid topicId, Guid messageId,
CancellationToken cancellationToken);
Task<bool> DeleteMessage(
string userId, Guid topicId, Guid messageId, CancellationToken cancellationToken);

/// <summary>
/// Deletes all messages associated with the topic.
/// </summary>
/// <param name="userId">The user ID.</param>
/// <param name="topicId">The topic ID.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>The task result contains a boolean value indicating the success of the deletion.</returns>
Task<bool> ClearMessages(string userId, Guid topicId, CancellationToken cancellationToken);

/// <summary>
/// Saves a user or system message along with an assistant message.
Expand Down
8 changes: 8 additions & 0 deletions OpenAI.ChatGpt/Interfaces/ITopicStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ Task EditTopicName(
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean value indicating the success of the deletion.</returns>
Task<bool> DeleteTopic(string userId, Guid topicId, CancellationToken cancellationToken);

/// <summary>
/// Deletes all topics and messages associated with a user.
/// </summary>
/// <param name="userId">The user ID.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>The task result contains a boolean value indicating the success of the deletion.</returns>
public Task<bool> ClearTopics(string userId, CancellationToken cancellationToken);

/// <summary>
/// Generates a new unique topic ID.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.ChatGpt/OpenAI.ChatGpt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageId>OpenAI.ChatGPT</PackageId>
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI ChatGPT integration for .NET</Product>
<Version>2.0.3</Version>
<Version>2.1.0</Version>
<Description>.NET integration for ChatGPT with streaming responses supporting (like ChatGPT) via async streams.</Description>
<RepositoryUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading

0 comments on commit a035c2b

Please sign in to comment.