In this new post, I explore how to write a ChatGPT client in C# using the OpenAI API. Also, walk through the process of creating a ChatGPT client in C# that can generate human-like responses to user queries. This ChatGPT client will also allow us to retain and send conversation history to the API, making it possible to maintain context and provide more personalized responses. I will also parameterize our ChatGPT client so that we can change whether or not to include the conversation history when calling the API, which language model to use, and the nature of the response.
As artificial intelligence continues to advance, language models such as ChatGPT have become increasingly powerful tools for natural language processing. ChatGPT is a large-scale neural language model that has been trained on massive amounts of text data and is capable of generating human-like responses to a wide variety of natural language prompts. It can be used for a range of applications, from chatbots and virtual assistants to language translation and content generation.
So, to demonstrate the write a ChatGPT client in C#, I will be creating a chatbot in a console application using C# and NET 7 and Spectre Console.
The source code of this post is available on GitHub.
Obtain an API key
First step, I have to obtain an API key from the OpenAI developer site. Go to the OpenAI developer website and register yourself.
After the successful registration, in your account on the top right, in the menu, you have View API keys. This option allows you to see the keys you already have or create a new one.
For example, in my API keys I can see the one I have created for this demo. If you want to create a new key, click on the button Create new secret key. A window pop up will appear with a new key. Save it because it is not possible to read it again.
ChatGPT has to retain the conversation Context
Before creating our client, I will briefly look into the ChatGPT API’s default behaviour and how it influences the design of our ChatGPT client.
Here is what happens if we don’t feed the API with all of the conversation’s previous messages:
By the time we ask our second question, the API has already forgotten what the conversation is about. But here’s what happens when we feed the API with all of the conversation’s previous messages:
By doing this, we are allowing the API to “remember” who “he” is and remember the context of the conversation. We can then design our ChatGPT C# client accordingly.
The implementation
First, I have to decide what version of the API I want to use. For this reason, I define an enum
with the version available so far.
public enum OpenAIModels
{
gpt_4,
gpt_4_0314,
gpt_4_32k,
gpt_4_32k_0314,
gpt_3_5_turbo,
gpt_3_5_turbo_0301
}
Response
After that, I have to receive the response from ChatGTP and I refer to the documentation to create the classes.
public class ChatResponse
{
public string? Id { get; set; }
public string? Object { get; set; }
public int Created { get; set; }
public List<Choice>? Choices { get; set; }
public Usage? Usage { get; set; }
}
public class Choice
{
public int Index { get; set; }
public Message? Message { get; set; }
public string? Finish_Reason { get; set; }
}
public class Message
{
public string? Role { get; set; }
public string? Content { get; set; }
}
public class Usage
{
public int Prompt_Tokens { get; set; }
public int Completion_Tokens { get; set; }
public int Total_Tokens { get; set; }
}
The ChatGPTClient
The SendMessage method is what we will call in order to generate our responses. This method calls the Chat method which sends our request to the API. Within the chat method, GetMessageObjects is called which retrieves either the current message only or all of the messages in the conversation. This allows the ChatGPT API to remember the context of the conversation. After the request is made, our message and the subsequent response are then saved to history using the AddMessageToHistory method.
public class ChatGPTClient
{
#region Variables
private readonly string chatRequestUri;
private readonly bool includeHistoryWithChatCompletion;
private readonly List<Message> messageHistory;
private readonly OpenAIModels model;
private readonly string openAIAPIKey;
private readonly double temperature;
private readonly double top_p;
#endregion Variables
public ChatGPTClient(bool includeHistoryWithChatCompletion = true,
OpenAIModels model = OpenAIModels.gpt_3_5_turbo,
double temperature = 1,
double top_p = 1)
{
chatRequestUri = "https://api.openai.com/v1/chat/completions";
openAIAPIKey = Environment.GetEnvironmentVariable("OpenAIAPIKey")!;
messageHistory = new List<Message>();
this.includeHistoryWithChatCompletion = includeHistoryWithChatCompletion;
this.model = model;
this.temperature = temperature;
this.top_p = top_p;
}
public async Task<ChatResponse?> SendMessage(string message)
{
var chatResponse = await Chat(message);
if (chatResponse != null)
{
AddMessageToHistory(new Message { Role = "user", Content = message });
foreach (var responseMessage in chatResponse.Choices!.Select(c => c.Message!))
AddMessageToHistory(responseMessage);
}
return chatResponse;
}
private void AddMessageToHistory(Message message) =>
messageHistory.Add(message);
private async Task<ChatResponse?> Chat(string message)
{
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, chatRequestUri);
request.Headers.Add("Authorization", $"Bearer {openAIAPIKey}");
var requestBody = new
{
model = GetModel(),
temperature,
top_p,
messages = GetMessageObjects(message)
};
request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{
var chatResponse = await response.Content.ReadFromJsonAsync<ChatResponse>();
if (chatResponse != null &&
chatResponse.Choices != null &&
chatResponse.Choices.Any(c => c.Message != null))
return chatResponse;
}
return null;
}
private IEnumerable<object> GetMessageObjects(string message)
{
foreach (var historicMessage in includeHistoryWithChatCompletion ? messageHistory : Enumerable.Empty<Message>())
{
yield return new { role = historicMessage.Role, content = historicMessage.Content };
}
yield return new { role = "user", content = message };
}
private string GetModel() =>
model.ToString().Replace("3_5", "3.5").Replace("_", "-");
}