Skip to contents

Set the API key

To use ChatGPT via its API, you need to provide an API key. You can get one by creating an account at https://platform.openai.com/. You’ll get 5 USD worth of credit that expires after 3 months. This is enough to process around two and a half million English words.

set_api_key() # This will open a prompt. Paste your OpenAI API key into the prompt.

When using set_api_key(), your key is stored in plain text in the global .Renviron file as GPTWORKR_KEY, so it will apply to all your projects. My preferred way is to create a .Renviron file in the project directory and manually add the key there, GPTWORKR_KEY = your_key_here. This way, I can use different keys for different projects. If you do this while using git, remember to add .Renviron to your .gitignore, or else your key will be visible in the git repo.

Your key is sent unencrypted via HTTP, so your key is inherently a little exposed when using the OpenAI API.

Request a chat completion

gpt_req() requests one chat completion from the OpenAI API’s chat-completion endpoint. It’s most important input is a tibble with role and content columns.

messages <- tribble(
  ~role, ~content,
  "system", "You are a helpful assistant in the tutorial vignette of the R package gptworkr.",
  "assistant", "Hi, welcome to this tutorial! How can I help you today?",
  "user", "I want to know what OpenAI's chat completion endpoint is."
)
reply <- gpt_req(messages)
reply
#> # A tibble: 1 × 10
#>   gpt_response         gpt_request gpt_model gpt_finish_reason gpt_prompt_tokens
#>   <chr>                <named lis> <chr>     <chr>                         <int>
#> 1 "The OpenAI's chat … <tibble>    gpt-3.5-… stop                             61
#> # ℹ 5 more variables: gpt_completion_tokens <int>, gpt_total_tokens <int>,
#> #   gpt_id <chr>, gpt_created <int>, gpt_system_fingerprint <chr>

The reply is a tibble with one row, and the columns gpt_response, gpt_request, and gpt_model, plus some more columns that you can read about in OpenAI’s documentation of the chat completion response object. Let’s look at the first variables. gpt_response contains the full response:

reply$gpt_response[1]
#> [1] "The OpenAI's chat completion endpoint is a service provided by OpenAI's GPT-3 model that allows you to generate text based on a prompt you provide. This endpoint can be accessed using the OpenAI API by sending a POST request to the appropriate URL with your prompt and API key.\n\nIn the gptworkr package, you can use the `gpt_chat()` function to interact with the OpenAI's chat completion endpoint. This function allows you to provide a prompt and receive a completion from the GPT-3 model.\n\nWould you like to see an example of how to use the `gpt_chat()` function in the gptworkr package?"

gpt_request contains the tibble that was used to form the request.

reply$gpt_request[[1]]
#> # A tibble: 3 × 2
#>   role      content                                                             
#>   <chr>     <chr>                                                               
#> 1 system    You are a helpful assistant in the tutorial vignette of the R packa…
#> 2 assistant Hi, welcome to this tutorial! How can I help you today?             
#> 3 user      I want to know what OpenAI's chat completion endpoint is.

This might seem superfluous, but it’s important for reproducibility when using the other functions in gptworkr.

gpt_model simply contains which GPT model was used to calculate the reply. gptworkr defaults to gpt-3.5-turbo.

If you know the OpenAI API, you know there are other parameters like model, temperature, max_tokens, logit_bias. You can set these in gpt_req() as well.

Example data

gptworkr comes with the wines dataset, which we’ll use in the examples. We’ll only use the description column

wines |> select(description) |> head()
#> # A tibble: 6 × 1
#>   description                                                                   
#>   <chr>                                                                         
#> 1 "The Definition range brings the world's greatest wine styles to Majestic cus…
#> 2 "You’d think that it’s seriously tough to impress Jean Rijckaert, who was the…
#> 3 "Sauternes is Bordeaux’s most renowned dessert wine style – and this is a stu…
#> 4 "Amarone is one of Italy’s most celebrated wines. Hailing from the Valpolicel…
#> 5 "The Edouard Delaunay estate’s rich history dates back to 1893. Its wines wer…
#> 6 "Our Parcel Series wines are Majestic's best kept secret. They're for when to…

Put GPT to work

The function gpt() sends a gpt request with the same instructions to all entries in a column of strings. It works by iterating gpt_req() over all entries in the column.

#> # A tibble: 5 × 10
#>   gpt_response         gpt_request gpt_model gpt_finish_reason gpt_prompt_tokens
#>   <chr>                <named lis> <chr>     <chr>                         <int>
#> 1 light, silky, suppl… <tibble>    gpt-3.5-… stop                            174
#> 2 fresh, creamy, mine… <tibble>    gpt-3.5-… stop                            224
#> 3 stunning, coveted, … <tibble>    gpt-3.5-… stop                            196
#> 4 celebrated, concent… <tibble>    gpt-3.5-… stop                            156
#> 5 rich, choice, fines… <tibble>    gpt-3.5-… stop                            173
#> # ℹ 5 more variables: gpt_completion_tokens <int>, gpt_total_tokens <int>,
#> #   gpt_id <chr>, gpt_created <int>, gpt_system_fingerprint <chr>

Now it’s more opaque exactly which requests were sent to GPT. So let’s inspect one of them:

adjectives$gpt_request[[1]] |> tinytable::tt()
tinytable_q2lroet2p3ll0uo5026c
role content
system You are a helpful assistant that will be provided with a description of a wine. Please answer with a comma separated list of all adjectives used to describe the wine.
user The Definition range brings the world's greatest wine styles to Majestic customers. Fleurie is the sub-region of Beaujolais where you'll find Gamay at its light, silky and supple best. To capture this style at its finest, we went to Pardon et Fils, a seventh-generation winery with some of the most experience in all of Beaujolais. To preserve all of its floral aromas, they left it completely unoaked. It's fresh and fruity, with flavours of red fruits, roses and violets. This is one of the few red wines that's light enough to serve with fish.

We see that the gpt() function makes a system message from the instruction argument and a user message from the data column.

Classification

You may try to instruct gpt() to classify with predetermined classes. This works surprisingly well, something like this:

food_pairings <- gpt(
  wines$description[1:5],
  "What food goes with this wine? Answer with words from the following list. No other words are allowed. {red meat, poultry, fish, other}"
)

But when I finally thought I had found a prompt that worked, it would reply venison to the 187th item or have very chatty responses from time to time.

gpt_classify() avoids this by implementing the “logit bias trick”:

country <- gpt_classify(
  data = wines$description[1:5],
  instruction = "You are a helpful assistant. Where does the wine come from?",
  classes = c("Narnia", "Atlantis", "Middle Earth")
)
country
#> # A tibble: 5 × 10
#>   gpt_response gpt_request      gpt_model    gpt_finish_reason gpt_prompt_tokens
#>   <fct>        <named list>     <chr>        <chr>                         <int>
#> 1 Middle Earth <tibble [3 × 2]> gpt-3.5-tur… length                          188
#> 2 Middle Earth <tibble [3 × 2]> gpt-3.5-tur… length                          238
#> 3 Middle Earth <tibble [3 × 2]> gpt-3.5-tur… length                          210
#> 4 Middle Earth <tibble [3 × 2]> gpt-3.5-tur… length                          170
#> 5 Middle Earth <tibble [3 × 2]> gpt-3.5-tur… length                          187
#> # ℹ 5 more variables: gpt_completion_tokens <int>, gpt_total_tokens <int>,
#> #   gpt_id <chr>, gpt_created <int>, gpt_system_fingerprint <chr>

See, it really works!

The request sent to GPT is more advanced in this case:

country$gpt_request[[1]] |> tinytable::tt()
tinytable_3zfxv936gj3c74a0wyly
role content
system You are a helpful assistant. Where does the wine come from? 1. Narnia 2. Atlantis 3. Middle Earth
user The Definition range brings the world's greatest wine styles to Majestic customers. Fleurie is the sub-region of Beaujolais where you'll find Gamay at its light, silky and supple best. To capture this style at its finest, we went to Pardon et Fils, a seventh-generation winery with some of the most experience in all of Beaujolais. To preserve all of its floral aromas, they left it completely unoaked. It's fresh and fruity, with flavours of red fruits, roses and violets. This is one of the few red wines that's light enough to serve with fish.
assistant The most likely choice for the data and context provided above is choice number

So gpt_classify() builds its request like this:

role content
system instruction + numbered list of the classes
user string from the data
assistant A reminder. The default reminder is “The most likely choice for the data and context provided above is choice number”

You can customize this. If you set append_classes to FALSE, the system message will only contain the instruction, not the numbered list of the classes. This is useful if you need to add definitions or information about the classes:

gpt_classify(data = wines$description[1:5],
             instruction = "Where is the wine from?\nCountry 1: Narnia. Narnian wines are acidic and bitter.\nCountry 2: Atlantis. Atlantean wines are sweet.",
             classes = c("Narnia","Atlantis"),
             append_classes = FALSE
             )

To change the third message, the assistant message, set the reminder argument. The third message is called a reminder because it comes after the data and reminds GPT that it should reply with a number. This can be any sentence that encourages GPT to reply with a number as the next token.

gpt_classify(data = wines$description[1:5],
             instruction = "Where is the wine from?\nCountry 1: Narnia. Narnian wines are acidic and bitter.\nCountry 2: Atlantis. Atlantean wines are sweet.",
             classes = c("Narnia","Atlantis"),
             reminder = "Considering the above information, the wine's most likely origin is Country "
             )

Loose ends

You can calculate what an analysis costs by feeding the response tibble into the costs function.

costs(adjectives)
#> [1] 0.00058

This number is in US $.

A WARNING

gptworkr does not yet control how quickly requests are sent. This is a problem because the OpenAI API has rate limits. My experience is that you will never overstep the rate limit per minute, because gptworkr sends requests sequentially and not in parallel, but you may overstep the rate limit per day. For example, if you haven’t used the API much, your rate limit is 10 000 requests per day with many models. If you overstep this limit, the API will send an error, gptworkr won’t catch it, and your call to gpt() or gpt_classify() will end without giving you any usable data. If this occurs in the, say, 2000th entry processed, this can cost you some $.