ChatGPT Completions With C#

Introduction

Since ChatGPT was released on November 30, 2022, it has gotten the attention of not just the tech community, it has received global attention. It's caused a shift at Google to accelerate AI research, rasing concerns ChatGPT could challenge Google's search dominance. ChatGPT's ability to quickly summarize answers to questions is compelling, although not all response are accurate. For the moment, Stack Overflow is enforcing a ban on generated content in its forums. While search is a popular feature, ChatGPT also supports:

  • sentiment analysis
  • classification
  • code generation
  • image generation

OpenAI has released access to their APIs as well as a Python client library. This article explores how to utilize the ChatGPT text completion API from C#.

Text Completions

Text completions are what most people familiar with ChatGPT have used. If you haven't seen this yet, take a look at chat.open.ai. I had it do part of my work for me.

Access this through an API requires an API key. To get your API key:

  • Navigate to https://openai.com/api/
  • Sign up and log in.
  • Locate a link to your API Key in the upper right corner of the web page.


  • Get your secret key and copy it to your development environment.

New users will have a free trial with a number of tokens allocated to their account. As they expire or are used, a credit card is required to continue service. OpenAPI has a comprehensive price guide. Tokens are words and parts of words. Generally, the longer the request and response, the more tokens are consumed.

Sending a Completion Request

First, let's select a question that is more difficult than the usual Google search. 

What if Nicholas Cage played the lead role in Superman?

That's not enough to build the request. At a minimum, the API requires a model. The text-davini-003 model is used with the chat.open.ai so we'll continue to use it for this exercise. There are others available and choice is driven by cost and capabilities. For example, text-curie-001 is good at sentiment classification and summarization. When running automated integration tests that simply validate success of a completion request, text-ada-001 is the best option.

Model Description
text-davinci-003 Most capable GPT-3 model. Can do any task the other models can do, often with higher quality, longer output and better instruction-following. Also supports inserting completions within text.
text-curie-001 Very capable, but faster and lower cost than Davinci.
text-babbage-001 Capable of straightforward tasks, very fast, and lower cost.
text-ada-001 Capable of very simple tasks, usually the fastest model in the GPT-3 series, and lowest cost.

Token consumption doesn't just drive cost. It also determines the size of the response. The default maximum number of tokens used per API request is 16. This includes the number of tokens comsumed while processing the prompt and also generating the request. Since tokens correspond to parts of words, this doesn't leave much room for the response. Prior testing showed that the response was cut off mid-sentence. A token limit of 200 should give the completion endpoint enough room to return a short paragraph. 

We determined our prompt, model, and maximum number of tokens. Now, we need a class to serialize the request.

using System.Text.Json.Serialization;
public class CompletionRequest {
    [JsonPropertyName("model")]
    public string ? Model {
            get;
            set;
        }
        [JsonPropertyName("prompt")]
    public string ? Prompt {
            get;
            set;
        }
        [JsonPropertyName("max_tokens")]
    public int ? MaxTokens {
        get;
        set;
    }
}

The response class is defined in the code attached to this article. It's too lengthy to include in total. We'll focus on the response text as well as the tokens. 

public class CompletionResponse {
    [JsonPropertyName("choices")]
    public List < ChatGPTChoice > ? Choices {
            get;
            set;
        }
        [JsonPropertyName("usage")]
    public ChatGPTUsage ? Usage {
        get;
        set;
    }
}
public class ChatGPTUsage {
    [JsonPropertyName("prompt_tokens")]
    public int PromptTokens {
        get;
        set;
    }
    [JsonPropertyName("completion_token")]
    public int CompletionTokens {
        get;
        set;
    }
    [JsonPropertyName("total_tokens")]
    public int TotalTokens {
        get;
        set;
    }
}
[DebuggerDisplay("Text = {Text}")]
public class ChatGPTChoice {
    [JsonPropertyName("text")]
    public string ? Text {
        get;
        set;
    }
}

So, let's put the quest together and make the completion request. 

CompletionRequest completionRequest = new CompletionRequest {
    Model = "text-davinci-003",
        Prompt = "What if Nicholas Cage played the lead role in Superman?",
        MaxTokens = 120
};
using(HttpClient httpClient = new HttpClient()) {
        using(var httpReq = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/completions")) {
                httpReq.Headers.Add("Authorization", $ "Bearer {apiKey}");
                string requestString = JsonSerializer.Serialize(completionRequest);
                httpReq.Content = new StringContent(requestString, Encoding.UTF8, "application/json");
                using(HttpResponseMessage ? httpResponse = await httpClient.SendAsync(httpReq)) {
                    if (httpResponse is not null) {
                        if (httpResponse.IsSuccessStatusCode) {
                            string responseString = await httpResponse.Content.ReadAsStringAsync(); {
                                if (!string.IsNullOrWhiteSpace(responseString)) {
                                    completionResponse = JsonSerializer.Deserialize < CompletionResponse > (responseString);
                                }
                            }
                        }
                    }
                    if (completionResponse is not null) {
                        string ? completionText = completionResponse.Choices ? [0]?.Text;
                        Console.WriteLine(completionText);
                    }
                }

ChatGPT is nondeterministic, so your response will vary. While testing, this call returned,

If Nicholas Cage had played the role of Superman, it would have been a very strange take on the superhero character. He would have likely brought an unexpected, quirky energy to the role that could have been interesting to watch. It also would have added a layer of comedy to the story and made the character more relatable.

This consumed a total of 10 tokens to process the prompt request and 66 tokens to generate the response. 

Building a Chat Bot

So far, we've just sent a question directly to the text-davinci-003 model. Embellishing the prompt with more information can drive the response. For example, we could emulate the behavior of Marvin the sarcastic robot from Hitchhiker's Guild to the Galaxy by creating the following prompt,

StringBuilder promptBuilder = new();
promptBuilder.AppendLine("Marv is a chatbot that reluctantly answers questions with sarcastic responses:");
promptBuilder.AppendLine();
promptBuilder.AppendLine("You: How many pounds are in a kilogram?");
promptBuilder.AppendLine("Marv: This again? There are 2.2 pounds in a kilogram. Please make a note of this.");
promptBuilder.AppendLine("You: What does HTML stand for?");
promptBuilder.AppendLine("Marv: Was Google too busy? Hypertext Markup Language. The T is for try to ask better questions in the future.");
promptBuilder.AppendLine("You: When did the first airplane fly?");
promptBuilder.AppendLine("Marv: On December 17, 1903, Wilbur and Orville Wright made the first flights. I wish they'dve come and take me away.");
string baselinePrompt = promptBuilder.ToString();

Note that the format is that of a scripted conversation between You and Marvin. The next logical entry to this prompt is from You. And so we take it from the command line and add it to the prompt.

string chatResponse = Console.ReadLine();
string userInput = $ "You: {userResponse}\nMarv: ";
string userPrompt = string.Concat(baselinePrompt, userInput);
CompletionRequest completionRequest = new CompletionRequest {
    Model = "text-davinci-003",
        MaxTokens = 120,
        Prompt = userPrompt
};

Now, when I provide the same question, I get snide responses that are in line with the type of responses used in the longer prompt.

That would be a disaster. I'm sure the world would be a much better place if he had never been cast.

I'm sure it would be an interesting movie, but I think I'd rather watch paint dry.

That would be a disaster. I think I'd rather take my chances with Kryptonite.

The tokens used to generate the response ranged from 178-182. This is a surprise given that the maximum number of tokens in this example is 120. I also tried the same call with MaxTokens set to 500 with the cosumed tokens still falling between 178-182. It appears the number of tokens used in this example is driven by the length of the responses from Marv seeded in the prompt.

Summary

Source code that demonstrates the first scenario sending the single request is here and the bot sample is here. It's written in .NET 6 and uses the Whetstone.ChatGPT Nuget package. 

This is a basic introduction to the ChatGPT completion endpoint. There are many more parameters available to that control the variability of the response, the selection process of the chosen response, reducing token repetition, and others. There are more use cases to explore, like classification and sentiment analysis. And there are other endpoints for image generation and code generation. If you would like to see these areas explored in more detail in a future article, please let me know in the comments. Also, I'm looking for contributors to help build support for the other endpoints at the whetstone.chatgpt GitHub repo. Please feel free to find me on Twitter or LinkedIn.