Mastering OpenAI's ChatGPT API with R6 Classes in R
Mark Vanderstay
Data Science Professional with a Unique Blend of Skills and Proven Track Record of Impact - Biostatistics, Psychology, Ecommerce
In this post I'll explore a structured approach to interacting with the OpenAI ChatGPT API using R6 classes. This method enhances the cleanliness, organisation, and maintainability of your scripts. Here's what we'll cover:
By the end of this post, you'll understand why R6 classes can be a powerful tool for interacting with APIs like OpenAI's ChatGPT.
I'll begin with a list of requirements:
Why R6 Classes?
R6 classes provide a robust and flexible framework for object-oriented programming in R, making them a valuable tool for many programming tasks, including API interactions like with OpenAI's ChatGPT.
Here are some key benefits:
R6 classes in R offer a modern, simple, and clean approach to object-oriented programming. They're not perfect, and can be confusing for beginners but if you persist you'll see that using an OOP approach in R is worthwhile.
Key Classes:
My approach uses two key R6 classes: MessageHistory and ChatGPT.
By encapsulating these two distinct pieces of functionality within separate R6 classes, we ensure that our code is well-structured and clearly organised. Each class has a specific, well-defined responsibility, making it easier to understand what each part of our code does. Furthermore, our code becomes more modular, allowing different components to be reused or replaced without affecting the rest of the codebase. Instead of one monolithic script, the result is clean, re-usable code that can be easily modified and improved.
The MessageHistory Class
Below is a walkthrough of this class and its methods:
library(R6
library(rlist)
# Define the MessageHistory class
MessageHistory <- R6Class("MessageHistory",
? ? ? ? ? ? ? ? ? private = list(
? ? ? ? ? ? ? ? ? ? # Private list that will contain the history of messages
? ? ? ? ? ? ? ? ? ? .history = list()
? ? ? ? ? ? ? ? ? ),
? ? ? ? ? ? ? ? ? public = list(
? ? ? ? ? ? ? ? ? ? # Public reference to the private .history list
? ? ? ? ? ? ? ? ? ? message_history = list(),
? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? # Constructor for the MessageHistory class which initialises the .history list
? ? ? ? ? ? ? ? ? ? initialize = function() {
? ? ? ? ? ? ? ? ? ? ? private$.history <- list()
? ? ? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? # Method to add a new message to the history.
? ? ? ? ? ? ? ? ? ? # The role and content of the message are passed as arguments.
? ? ? ? ? ? ? ? ? ? add_message = function(role, content) {
? ? ? ? ? ? ? ? ? ? ? private$.history <- c(private$.history, list(list(role = role, content = content)))
? ? ? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? # Method to return the full message history
? ? ? ? ? ? ? ? ? ? get_history = function() {
? ? ? ? ? ? ? ? ? ? ? return(private$.history)
? ? ? ? ? ? ? ? ? ? },
? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? # Method to return the last message from a given role.
? ? ? ? ? ? ? ? ? ? # The method iterates over the history from the end until it finds a message
? ? ? ? ? ? ? ? ? ? # from the specified role and then returns it.
? ? ? ? ? ? ? ? ? ? # If no message from the specified role is found, the method returns NULL.
? ? ? ? ? ? ? ? ? ? get_last_message = function(role) {
? ? ? ? ? ? ? ? ? ? ? for (i in length(private$.history):1) {
? ? ? ? ? ? ? ? ? ? ? ? if (private$.history[[i]]$role == role) {
? ? ? ? ? ? ? ? ? ? ? ? ? return(private$.history[[i]]$content)
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? return(NULL)
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? )
))
Private Members
Within the?MessageHistory?class, we have a private member?.history. This is a list that will store each message as an element. Each element is a list itself with two items: 'role' and 'content'. It is marked as private to prevent it from being directly manipulated outside of the class methods.
Public Methods
Let's move on to the?ChatGPT?class and see how it utilises?MessageHistory?to communicate effectively with the OpenAI ChatGPT API.
领英推荐
The ChatGPT Class
This class is responsible for interacting with the OpenAI ChatGPT API, processing the user's messages and returning the model's responses.
library(R6
library(httr)
library(jsonlite)
source("./classes/MessageHistory.R")
# Define the ChatGPT class
ChatGPT <- R6Class("ChatGPT",
? ? ? ? ? ? ? ? ? ?private = list(
? ? ? ? ? ? ? ? ? ? ?# Private string that will store the API token
? ? ? ? ? ? ? ? ? ? ?.api_token = NULL,??
? ? ? ? ? ? ? ? ? ? ?# Private string that will store the model name
? ? ? ? ? ? ? ? ? ? ?.model = NULL,??
? ? ? ? ? ? ? ? ? ? ?# API endpoint
? ? ? ? ? ? ? ? ? ? ?.url = "https://api.openai.com/v1/chat/completions",??
? ? ? ? ? ? ? ? ? ? ?# Instance of the MessageHistory class to store messages
? ? ? ? ? ? ? ? ? ? ?.message_list = NULL,??
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?# Private method to generate the headers for the HTTP request
? ? ? ? ? ? ? ? ? ? ?get_headers = function() {
? ? ? ? ? ? ? ? ? ? ? ?httr::add_headers(
? ? ? ? ? ? ? ? ? ? ? ? ?"Content-Type" = "application/json",
? ? ? ? ? ? ? ? ? ? ? ? ?"Authorization" = paste("Bearer", private$.api_token)
? ? ? ? ? ? ? ? ? ? ? ?)
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ?),
? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ?public = list(
? ? ? ? ? ? ? ? ? ? ?# Public object to store the raw response from the API
? ? ? ? ? ? ? ? ? ? ?response = NULL,??
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?# Constructor for the ChatGPT class
? ? ? ? ? ? ? ? ? ? ?initialize = function(api_token, model) {
? ? ? ? ? ? ? ? ? ? ? ?private$.api_token <- api_token
? ? ? ? ? ? ? ? ? ? ? ?private$.model <- model
? ? ? ? ? ? ? ? ? ? ? ?private$.message_list <- MessageHistory$new()
? ? ? ? ? ? ? ? ? ? ?},
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?# Method to send a message to the API and get a response
? ? ? ? ? ? ? ? ? ? ?chat = function(message) {
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Add user message to the message list
? ? ? ? ? ? ? ? ? ? ? ?private$.message_list$add_message("user", message)
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Prepare the body for the API request
? ? ? ? ? ? ? ? ? ? ? ?body <- list(
? ? ? ? ? ? ? ? ? ? ? ? ?"model" = private$.model,
? ? ? ? ? ? ? ? ? ? ? ? ?"messages" = private$.message_list$get_history()
? ? ? ? ? ? ? ? ? ? ? ?)
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Send the API request and store the response
? ? ? ? ? ? ? ? ? ? ? ?self$response <- POST(private$.url, private$get_headers(), body = toJSON(body, auto_unbox = TRUE), encode = "json")
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Extract the assistant's message from the response
? ? ? ? ? ? ? ? ? ? ? ?content <- content(self$response, "parsed")
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Add the assistant's response to our message list
? ? ? ? ? ? ? ? ? ? ? ?private$.message_list$add_message("assistant", content$choices[[1]]$message$content)
? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ?# Return the assistant's message
? ? ? ? ? ? ? ? ? ? ? ?return(content$choices[[1]]$message$content)
? ? ? ? ? ? ? ? ? ? ?},
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?# Method to retrieve the conversation history
? ? ? ? ? ? ? ? ? ? ?history = function(){
? ? ? ? ? ? ? ? ? ? ? ?return(private$.message_list$get_history())
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ?)
))
Private Members
In the?ChatGPT?class, we have private members for the API token, model name, API URL, and an instance of the?MessageHistory?class (message_list). These variables are marked private to protect them from direct external manipulation, a feature that helps to maintain data integrity. We also have a private method, get_headers(), which generates the headers required for the API request.
Public Methods
The Power of Using Classes Over Simple Scripts
As I explained above, I chose to design this interaction with R6 classes rather than writing a simple script to handle API calls. Here are four good reasons:
So the?ChatGPT?class empowers us to interact with the OpenAI API in an organised, scalable, and secure manner. The combination of?ChatGPT?and?MessageHistory?classes provides an excellent example of how we can use object-oriented programming concepts in R to interact with an API like ChatGPT.?
In the next section, I'll show you how to create an instance of this class and interact with it, bringing these concepts together into a complete and functional example.
An Example
Now that we have our?MessageHistory?and?ChatGPT?classes, let's put them to use. The following is an example of how to create an instance of?ChatGPT?and interact with it:
# Load necessary libraries
library(R6)
source("./classes/MessageHistory.R")
source("./classes/ChatGPT.R")
# Create an instance of the ChatGPT class
chat_instance <- ChatGPT$new(api_token = Sys.getenv("<YOUR_API_TOKEN>"), model = "gpt-3.5-turbo-0301")
# Send a message and get a response
response <- chat_instance$chat("Tell me a joke.")
print(response) # The model's response will be printed
# Send another message and get a response with the appropriate context!
# i.e. ChatGPT will know we're referring to jokes.
response <- chat_instance$chat("Tell me another.")
print(response) # The model's response will be printed.
# Print the entire conversation history
history <- chat_instance$history()
print(history) # The conversation history will be printeds
First, we create an instance of the?ChatGPT?class by calling?ChatGPT$new(), passing the API token and model name as arguments. This instance gives us access to the?chat()?and?history()?methods of the?ChatGPT?class. I've stored my API token in an?.Renviron?file and passed it in via?Sys.getenv?to avoid committing it to GitHub.
We use?chat()?to send messages to the API and get responses. In this example, we've asked for a joke and the model's response is printed. We then ask for another joke, and again the model's response is printed.
Finally, we call?history()?to retrieve the entire conversation history, which we print out.
This example illustrates how our classes encapsulate functionality and data into reusable and understandable blocks. We could extend this basic interaction, for example, by creating a loop to continue the conversation for a set number of turns, or by adding error handling to ensure the program behaves gracefully if something unexpected occurs.
These classes provide a solid foundation on which to build a range of applications that interact with the ChatGPT API, while maintaining a clear, organised code structure. This structured approach is one of the many advantages of using R6 classes in R and demonstrates how they can be used to simplify interactions with APIs like OpenAI's ChatGPT.
Next time you're considering using a script to interact with an API, consider using R6 classes instead. They offer many benefits that can make your code clearer, more maintainable, and more robust.
P.S. The classes above are limited in their functionality and robustness in order to keep this post to the point. Some areas I plan to improve are;