Skip to content

Commit

Permalink
Add ContinueOrStartNewTopic method to ChatGPT API
Browse files Browse the repository at this point in the history
  • Loading branch information
rodion-m committed Mar 21, 2023
1 parent 2083216 commit bf52335
Show file tree
Hide file tree
Showing 15 changed files with 82 additions and 68 deletions.
72 changes: 41 additions & 31 deletions OpenAI.ChatGpt/ChatGPT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace OpenAI.ChatGpt;

/// <summary> Chat conversations provider </summary>
public class ChatGPT : IDisposable
{
private readonly string _userId;
Expand All @@ -11,88 +12,102 @@ public class ChatGPT : IDisposable
private readonly OpenAiClient _client;
private Chat? _currentChat;

/// <summary>
/// Use this constructor to create chat conversation provider for the specific user.
/// </summary>
public ChatGPT(
OpenAiClient client,
string userId,
IMessageStore messageStore,
ChatCompletionsConfig? config)
{
_client = client;
_client = client ?? throw new ArgumentNullException(nameof(client));
_userId = userId ?? throw new ArgumentNullException(nameof(userId));
_messageStore = messageStore ?? throw new ArgumentNullException(nameof(messageStore));
_config = config;
}

/// <summary>
/// If your don't have users use this ChatGPT constructor.
/// If you don't have users use this ChatGPT constructor.
/// </summary>
public ChatGPT(
OpenAiClient client,
IMessageStore messageStore,
ChatCompletionsConfig? config)
{
_client = client;
_userId = Guid.Empty.ToString();
_client = client ?? throw new ArgumentNullException(nameof(client));
_messageStore = messageStore ?? throw new ArgumentNullException(nameof(messageStore));
_userId = Guid.Empty.ToString();
_config = config;
}


/// <summary>
/// If you don't have users and don't want to save messages into database use this method.
/// </summary>
public static Task<Chat> CreateInMemoryChat(
string apiKey,
ChatCompletionsConfig? config = null,
ChatCompletionDialog? initialDialog = null)
UserOrSystemMessage? initialDialog = null)
{
if (apiKey == null) throw new ArgumentNullException(nameof(apiKey));
var client = new OpenAiClient(apiKey);
var chatGpt = new ChatGPT(client, new InMemoryMessageStore(), config);
return chatGpt.StartNewTopic(initialDialog: initialDialog);
}

public void Dispose()
{
_currentChat?.Dispose();
}

// private async Task<Chat> ContinueOrStartNewChat(CancellationToken cancellationToken = default)
// {
// if (_currentChat is not null) return _currentChat;
// var id = await _messageStore.GetLastChatIdOrNull(_userId, cancellationToken);
// return id is null
// ? await StartNewChat(cancellationToken: cancellationToken)
// : CreateChat(id.Value, false, null);
// }
/// <summary> Continues the last topic or starts a new one.</summary>
public async Task<Chat> ContinueOrStartNewTopic(
DateTimeOffset? createdAt = null,
CancellationToken cancellationToken = default)
{
if (_currentChat is not null) return _currentChat;
var topic = await _messageStore.GetLastTopicOrNull(_userId, cancellationToken);
return topic is null
? await StartNewTopic(cancellationToken: cancellationToken)
: await SetTopic(topic, cancellationToken);
}

/// <summary> Starts a new topic. </summary>
public async Task<Chat> StartNewTopic(
string? name = null,
ChatCompletionsConfig? config = null,
ChatCompletionDialog? initialDialog = null,
UserOrSystemMessage? initialDialog = null,
DateTimeOffset? createdAt = null,
CancellationToken cancellationToken = default)
{
var topic = new Topic(_messageStore.NewTopicId(), _userId, name, createdAt ?? DateTimeOffset.Now,
ChatCompletionsConfig.CombineOrDefault(_config, config));
createdAt ??= DateTimeOffset.Now;
config = ChatCompletionsConfig.CombineOrDefault(_config, config);
var topic = new Topic(_messageStore.NewTopicId(), _userId, name, createdAt.Value, config);
await _messageStore.AddTopic(_userId, topic, cancellationToken);
if (initialDialog is not null)
{
await _messageStore.SaveMessages(_userId, topic.Id, initialDialog.GetMessages(), cancellationToken);
}

//await _messageStore.SetCurrentChatId(_userId, topicId, cancellationToken);
_currentChat = CreateChat(topic, true);
return _currentChat;
}

public async Task<Chat> SetTopic(
Guid topicId,
CancellationToken cancellationToken = default)

public async Task<Chat> SetTopic(Guid topicId, CancellationToken cancellationToken = default)
{
var topic = await _messageStore.GetTopic(_userId, topicId, cancellationToken);
if (topic is null)
{
throw new ArgumentException($"Chat with id {topicId} not found for user {_userId}");
}
return await SetTopic(topic, cancellationToken);
}

private Task<Chat> SetTopic(Topic topic, CancellationToken cancellationToken = default)
{
if (topic == null) throw new ArgumentNullException(nameof(topic));
_currentChat = CreateChat(topic, false);
await _messageStore.AddTopic(_userId, topic, cancellationToken);
//await _messageStore.SetCurrentChatId(_userId, topicId, cancellationToken);
return _currentChat;
return Task.FromResult(_currentChat);
}

private Chat CreateChat(Topic topic, bool isNew)
Expand All @@ -101,7 +116,7 @@ private Chat CreateChat(Topic topic, bool isNew)
return new Chat(_messageStore, _client, _userId, topic, isNew);
}

public async Task<IReadOnlyList<Topic>> GetChats(CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<Topic>> GetTopics(CancellationToken cancellationToken = default)
{
var chats = await _messageStore.GetTopics(_userId, cancellationToken);
return chats.ToList();
Expand All @@ -111,9 +126,4 @@ public void Stop()
{
_currentChat?.Stop();
}

public void Dispose()
{
_currentChat?.Dispose();
}
}
4 changes: 2 additions & 2 deletions OpenAI.ChatGpt/ChatGPTFactory.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net.Http.Headers;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
using OpenAI.ChatGpt.Models;

Expand All @@ -16,6 +15,7 @@ namespace OpenAI.ChatGpt;
/// .AddPolicyHandler(GetRetryPolicy())
/// .AddPolicyHandler(GetCircuitBreakerPolicy());
/// </example>
// ReSharper disable once InconsistentNaming
internal class ChatGPTFactory : IDisposable
{
private readonly OpenAiClient _client;
Expand Down Expand Up @@ -51,7 +51,7 @@ public ChatGPTFactory(
{
if (apiKey == null) throw new ArgumentNullException(nameof(apiKey));
_client = new OpenAiClient(apiKey);
_config = config ?? new ChatCompletionsConfig();
_config = config ?? ChatCompletionsConfig.Default;
_messageStore = messageStore ?? throw new ArgumentNullException(nameof(messageStore));
}

Expand Down
2 changes: 2 additions & 0 deletions OpenAI.ChatGpt/IMessageStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Task<IEnumerable<ChatCompletionMessage>> GetMessages(
CancellationToken cancellationToken
);

Task<Topic?> GetLastTopicOrNull(string userId, CancellationToken cancellationToken);

Task SaveMessages(string userId,
Guid topicId,
UserOrSystemMessage message,
Expand Down
11 changes: 11 additions & 0 deletions OpenAI.ChatGpt/InMemoryMessageStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ public Task<IEnumerable<ChatCompletionMessage>> GetMessages(string userId, Guid
return Task.FromResult(chatMessages.AsEnumerable());
}

public Task<Topic?> GetLastTopicOrNull(string userId, CancellationToken cancellationToken)
{
if (!_users.TryGetValue(userId, out var userChats))
{
return Task.FromResult<Topic?>(null);
}

var lastTopic = userChats.Values.MaxBy(x => x.CreatedAt);
return Task.FromResult(lastTopic);
}

public Task<IEnumerable<Topic>> GetTopics(string userId, CancellationToken cancellationToken)
{
if (!_users.TryGetValue(userId, out var topics))
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.ChatGpt/Models/ChatCompletionsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace OpenAI.ChatGpt.Models;

public class ChatCompletionsConfig
{
public static ChatCompletionsConfig Default { get; } = new()
public static ChatCompletionsConfig Default => new()
{
PassUserIdToOpenAiRequests = true
};
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.ChatGpt/Models/Topic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ private Topic() {}
#pragma warning restore CS8618


public Topic(
internal Topic(
Guid id,
string userId,
string? name,
Expand Down
4 changes: 3 additions & 1 deletion OpenAI.ChatGpt/OpenAI.ChatGpt.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
<PackageProjectUrl>https://github.com/rodion-m/ChatGPT_API_dotnet</PackageProjectUrl>
<Product>OpenAI API</Product>
<Version>2.0.0</Version>
<Description>.NET client for the OpenAI Chat Completions API (ChatGPT). It allows you to use the API in your .NET applications. Also, the client supports streaming responses (like ChatGPT) via async streams.</Description>
<Description>.NET client for the OpenAI Chat Completions API (ChatGPT) with easy DI 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>
<RootNamespace>OpenAI.ChatGpt</RootNamespace>
<Title>OpenAI ChatGPT SDK for .NET</Title>
<PackageTags>chatgpt, openai, sdk, apiclient, chatcompletions, gpt3, gpt4</PackageTags>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.Core/Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class Dialog
public static UserMessage StartAsUser(string userMessage)
{
if (userMessage == null) throw new ArgumentNullException(nameof(userMessage));
return new ChatCompletionDialog(userMessage);
return new UserMessage(userMessage);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ internal AssistantMessage(List<ChatCompletionMessage> messages, string content)
{
}

public ChatCompletionDialog ThenUser(string userMessage)
public UserMessage ThenUser(string userMessage)
{
if (string.IsNullOrWhiteSpace(userMessage))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(userMessage));
return new ChatCompletionDialog(Messages, userMessage);
return new UserMessage(Messages, userMessage);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ public UserMessage ThenUser(string userMessage)
{
if (string.IsNullOrWhiteSpace(userMessage))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(userMessage));
return new ChatCompletionDialog(Messages, userMessage);
return new UserMessage(Messages, userMessage);
}
}
4 changes: 2 additions & 2 deletions OpenAI.Core/OpenAIClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void Dispose()
}

public async Task<string> GetChatCompletions(
UserMessage dialog,
UserOrSystemMessage dialog,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default,
float temperature = ChatCompletionTemperatures.Default,
Expand Down Expand Up @@ -211,7 +211,7 @@ private static ChatCompletionRequest CreateChatCompletionRequest(
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Chunks of ChatGPT's response, one by one</returns>
public IAsyncEnumerable<string> StreamChatCompletions(
UserMessage messages,
UserOrSystemMessage messages,
int maxTokens = ChatCompletionRequest.MaxTokensDefault,
string model = ChatCompletionModels.Default,
CancellationToken cancellationToken = default)
Expand Down
2 changes: 1 addition & 1 deletion OpenAI.Test/ChatCompletionsApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public async void Stream_chatgpt_response_for_one_message_works()
[Fact]
public async void Stream_chatgpt_response_for_dialog_works()
{
ChatCompletionDialog dialog =
var dialog =
Dialog.StartAsUser("How many meters are in a kilometer? Write just the number.")
.ThenAssistant("1000")
.ThenUser("Convert it to hex. Write just the number.")
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ await foreach (string chunk in _client.StreamChatCompletions(new UserMessage(tex
## Continue dialog with ChatGPT (message history)
Use `ThenAssistant` and `ThenUser` methods to create a dialog:
```csharp
ChatCompletionDialog dialog =
Dialog.StartAsUser("How many meters are in a kilometer? Write just the number.") //the message from user
var dialog = Dialog.StartAsUser("How many meters are in a kilometer? Write just the number.") //the message from user
.ThenAssistant("1000") // response from the assistant
.ThenUser("Convert it to hex. Write just the number."); // the next message from user
Expand Down
22 changes: 13 additions & 9 deletions samples/ChatGpt.SpectreConsoleExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
using OpenAI;
using OpenAI.ChatGpt;
using OpenAI.ChatGpt.Models;
using Spectre.Console;

using Console = Spectre.Console.AnsiConsole;

Console.MarkupLine("[underline yellow]Welcome to ChatGPT Console![/]");
Console.MarkupLine("[underline red]Press Ctrl+C to stop writing[/]");

Console.WriteLine();

var apiKey = LoadApiKey();
Chat chat = await ChatGPT.CreateInMemoryChat(apiKey);

SetupCancellation();

var name = Console.Ask<string?>("What's your [green]name[/]?") ?? "Me";
var apiKey = LoadApiKey();
Chat chat = await ChatGPT.CreateInMemoryChat(
apiKey,
config: new ChatCompletionsConfig() { MaxTokens = 200 },
initialDialog: Dialog.StartAsSystem($"You are helpful assistant for a person named {name}.")
);
SetupCancellation(chat);

Console.MarkupLine("[underline yellow]Start chat. Now ask something ChatGPT...[/]");
while (AnsiConsole.Ask<string>($"[underline green]{name}[/]: ") is { } userMessage)
{
AnsiConsole.Markup("[underline red]ChatGPT[/]: ");
var stream = chat.StreamNextMessageResponse(userMessage)
.ThrowOnCancellation(false);
.ThrowOnCancellation(false);
await foreach (string chunk in stream.SkipWhile(string.IsNullOrWhiteSpace))
{
if(!chat.IsCancelled) Console.Write(chunk);
if (!chat.IsCancelled) Console.Write(chunk);
}

Console.WriteLine();
}

string LoadApiKey()
{
var s = Environment.GetEnvironmentVariable("openai_api_key_paid")
var s = Environment.GetEnvironmentVariable("openai_api_key_paid")
?? Console.Ask<string>("Please enter your [green]OpenAI API key[/] " +
"(you can get it from https://platform.openai.com/account/api-keys): ");

return s;
}

void SetupCancellation()
void SetupCancellation(Chat chat)
{
System.Console.CancelKeyPress += (_, args) =>
{
Expand Down

0 comments on commit bf52335

Please sign in to comment.