Mastering OpenAI's ChatGPT API with R6 Classes in R

Mastering OpenAI's ChatGPT API with R6 Classes in R

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:

  • Why R6 Classes? I'll discuss the benefits of using R6 classes in R for object-oriented programming.
  • Key Classes: I'll introduce two key classes, MessageHistory and ChatGPT, and explain their roles in managing chat messages and interacting with the OpenAI API.
  • Benefits of Using Classes Over Scripts: I'll highlight the advantages of using classes over simple scripts, including encapsulation, code reusability, data integrity, and ease of code expansion.
  • Example: I'll provide a link to a GitHub repository where you can find a complete and functional example of creating an instance of ChatGPT and interacting with it.

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:

  1. R Programming Knowledge: A basic understanding of R programming is essential. Familiarity with R6 classes and object-oriented programming concepts would be beneficial but not mandatory as the post provides a detailed explanation.
  2. OpenAI API Key: You'll need an API key from OpenAI to interact with the ChatGPT API. You can obtain this key by creating an account on the OpenAI platform and following their API documentation. There is a cost attached, of course.
  3. R and, optionally, RStudio: Ensure that you have the latest versions of R and RStudio installed on your computer. RStudio is not mandatory, but it provides a user-friendly interface for coding in R.
  4. R Packages: You'll need to install and load the R6, httr, and jsonlite packages in R. These packages provide the necessary functions to create R6 classes and interact with the API.
  5. Internet Connection: As you'll be interacting with the OpenAI API, a stable internet connection is required.
  6. GitHub Account (Optional): If you wish to clone the GitHub repository used in the post, you'll need a GitHub account. However, this is optional as you can also copy the code directly.
  7. Patience and Curiosity: As with learning any new concept, patience and a willingness to experiment and learn are crucial. Don't be afraid to modify the code and see what happens.


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:

  • Encapsulation: R6 classes bundle related data and operations into a single entity, enhancing code readability and manageability.
  • Inheritance: R6 supports inheritance, allowing you to create subclasses that inherit the properties and methods of a parent class. This promotes code reusability and logical structure.
  • Reference Semantics: Unlike S3 and S4 classes in R, R6 uses reference semantics, meaning that if you modify an object, the changes are reflected in all references to the object. This is a powerful feature for managing and updating complex data structures.
  • Private and Public Members: R6 classes allow for the creation of private and public members. Private members can only be accessed within the class, protecting them from external manipulation and maintaining data integrity.
  • Method Chaining: R6 classes support method chaining, allowing multiple operations to be performed in a single line of code. This can make your code more concise and easier to read.

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.

  1. MessageHistory Class: This class is responsible for managing the history of chat messages. Each message consists of a 'role' (i.e., whether it's a user or bot message) and the 'message' content. The class provides methods to add messages to the history and retrieve the last message by a given role. This class plays a pivotal role in maintaining the continuity of a conversation when interacting with the API. The OpenAI ChatGPT API uses the history of messages to maintain context for generating responses. In an interactive chat setting, each message you send to the API should include both the message you want to generate a response to, as well as the conversation history. The API generates responses based on the conversation history you provide; it does not store any context or conversation history itself. The MessageHistory class efficiently handles this aspect by maintaining a list of messages in the conversation, thus helping the ChatGPT class manage the conversation context when making API requests.
  2. ChatGPT Class: This class is designed to handle interactions with the OpenAI API. It accepts an API token and model upon initialisation and offers a chat method that makes the API request and returns the response. Crucially, it incorporates an instance of the MessageHistory class, demonstrating how different classes can be combined for more powerful and flexible code. This class is the main interface for sending user messages to the API and receiving responses from the model.

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

  • initialize(): This is the constructor for the?MessageHistory?class which is automatically called when a new instance of the class is created. It initialises the?.history?list to an empty list.
  • add_message(role, content): This method accepts two parameters - 'role' and 'content' - and appends them as a new list to the?.history?list. Each time a message is sent or received in the conversation, this method is used to add it to the history.
  • get_history(): This method returns the entire?.history?list, providing a full record of the conversation when called.
  • get_last_message(role): This method retrieves the most recent message from a specified role. It iterates through the?.history?list backwards and returns the content of the first message it encounters from the specified role. If no such message exists, it returns NULL.

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

  • initialize(api_token, model): The constructor of the?ChatGPT?class which sets the API token, model name and initializes the?message_list.
  • chat(message): This method takes in a user's message, adds it to the conversation history, sends the entire conversation history to the API, adds the model's response to the conversation history, and then returns the model's message. This is the heart of the class where the main interaction with the API happens.
  • history(): This method returns the full conversation history, providing transparency and the ability to review the conversation as needed.

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:

  • Encapsulation: By defining a class, we encapsulate related data and operations into a single entity. In our case, all operations related to the ChatGPT model (like setting the API token, sending a message, and getting the history) are part of the same?ChatGPT?class. This makes the code easier to understand, manage, and debug. Each object should ideally have just one concern.
  • Code reusability and modularity: Once the?ChatGPT?class is defined, it can be easily reused in any other part of your code or even in different projects. You just need to create a new instance of the class, and all methods and properties are readily available.
  • Data integrity: Using classes and private members, we ensure data safety. For instance, direct modification of the API token or the message list is not allowed, thus preventing unintentional or malicious alterations.
  • Code expansion: As new features or changes come to the API, adapting your code becomes a breeze. You just need to modify your class definition without impacting the rest of your code. This is particularly beneficial for larger projects or when working in a team.

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;

  • Ensuring the MessageHistory keeps with the token limits. Long conversations may exceed the model's token limit.
  • Error handling - as you can see there isn't much. Timeouts, disconnects and empty responses are not handled currently.
  • I also plan to create separate classes that can interact with these two. For example, a StoryTeller class that uses the ChatGPT class to generate stories, or a CustomerService class for interacting with customers. Let me know if you have others.


要查看或添加评论,请登录

社区洞察

其他会员也浏览了