Integrating OpenAI into a Ruby on Rails/React EdTech App

The craze around and promise of technological language models, such as OpenAI’s ChatGPT, is well deserved. After years of whittling and tailoring Google searches to find even vague answers to our myriad inquiries (often to only be met with unhelpful internet rabbit holes and paid advertisements), it is satisfying to finally feel understood in our quest for very specific knowledge by a ubiquitous and accessible technology. The general moral panics around these platforms (plagiarism, the reliance on technology resulting in the weakening of the human mind, hostile artificial intelligence takeovers, etc.) are akin to Socrates’ objections to the written word. The inherent goodness or evil that lies in these tools is in how we as humans regulate and use them–not in the tool themselves. But that is a blog post for another day.

For prescriptive or repetitive writing, the answer to a specific pondering, or an outline or prompt to boost the creative writing process, these tools are immeasurably helpful. As a former classroom educator and current tech worker and student, I wanted to create an app that would help teachers quickly write end-of-semester or end-of-year student reports as my Flatiron Capstone Project and knew that integrating OpenAI into this app would be a great fit. Any teacher passionate about their career will tell you that the experience is wonderful, but exhausting and replete with extra-curricular administrative and bureaucratic tasks. When I was teaching, writing reports for each of my students in each of my courses at the end of each semester filled me with a sense of dread that is hard to describe for folks outside of the profession. Educators often have anywhere from 50-150 students in their various courses, and creating detailed descriptions of each student’s behavioral and academic strengths and weaknesses is unbelievably tedious and slow. The goal of my Capstone app is to expedite the writing of these reports. By calling on OpenAI with an appropriate prompt and simple data for each student, this app harnesses the power of language models to generate a concise and generic starter report for each student that can then be further edited.

Integrating OpenAI into my Ruby on Rails/React app was challenging for me as a beginner programmer. While OpenAI has a generous usage policy and detailed documentation around Python and JavaScript integration, they do not offer official step-by-step documentation for full integration into a Ruby on Rails/React application. After much internet scouring, trial and error, and even calling upon ChatGPT itself to assist me, I cobbled together a fully functioning API call to OpenAI and was able to successfully return its content back to the client. For the sake of posterity, clarity, and all of the beginner Ruby on Rails developers out there, I am happy to document these steps here.

1. After creating a Rails/React application, add the necessary gems to your Gemfile that can handle API requests. There are a variety of gems that can do this, but I found the rest-client gem to be successful for my project. Type gem ‘rest-client’ into your Gemfile and run bundle install in the project directory in your terminal to install.

Add gem ‘rest-client’ to your Gemfile and bundle install.

2. Sign up for an OpenAI API account here and click API when you log in. This is the platform that provides an API key for your app, along with guidance and references for usage.

3. Generate a personal API Key from within your Profile. This is a secure key that is used in the API call. OpenAI will encrypt this key when it is generated.

OpenAI allows you to generate an API Key to use for your application.

4. Store your encrypted API key in your code editor. If your API key is not securely stored, it will work in development, but OpenAI will disable this key when you push your code to Github or another web service for production. To securely store your key,

a) In the config/environments/development.rb file, store your API key as an environment variable using the following language:

Store your API key in your config/environments/development.rb file.

b) Include this file in the .gitignore file. This will ensure that this file does not get pushed to production or Github and will prevent your secure key from being exposed.

Add the development.rb file to the .gitignore file.

c. For your code to work in production, add the OpenAI API key as an environment variable in your production environment. I used Render and was quickly able to add another environment variable to my web service:

Store your API Key in your production environment.

5. Create a ChatGPT class in your Rails application. I used rails generate to create a model of this in my app’s models folder. The method responsible for taking in the client’s prompt and information and returning a response is defined in this class. Again, for a beginner developer, this was a complex method to define and includes many steps. RestClient requires a few different arguments, which I will go over in detail.

a) Require your API handling gem at the top of your class.

Require ‘rest-client’ at the top of your ChatGPT class.

b) The first argument to supply to RestClient is the API URL. OpenAI has several different versions of Chat and Chat completions to use. The URL you supply RestClient is dependent on the version of ChatGPT you use. I used a recent model, gpt-3.5-turbo, which relies on the URL: https://api.openai.com/v1/chat/completions. I defined a variable with this URL in the first few lines of the Class:

Ensure you are using the correct Base URL for the Chat model you wish to employ.

c) Define the instance method that will take in a user’s prompt and return ChatGPT content. In the following snippet, I define the instance method and the second argument for RestClient–the headers. The headers use our securely stored API Key:

The headers argument will take in your securely stored API Key.

d) Create a data object, the last argument for RestClient. The data object stores information about the app’s model of ChatGPT, the roles of both ChatGPT (the system) and the user (which includes the user’s prompt), the length of the message to be returned (max_tokens), and the temperature (how random or focused the system is).

The data object sets the stage for ChatGPT’s response.

e) Finally, RestClient can generate the Chat response.

6. Generate a ChatController with rails generate. 

7. Create a custom route in config/routes.rb that routes to the ChatController.

8. Define the route in the ChatController that takes in the user’s prompt, calls upon the Class instance to generate a response, and returns the response as json.

9. Create a function on the front end that can take in either a hard-wired or dynamic prompt from the user. And voila!

Though a complex challenge for a beginner programmer, integrating OpenAI with my Ruby on Rails/React app allowed me to more deeply understand API calls, secure environment variables, and translating json between client and server in general. It has also increased the usability and ease of my app significantly, and I am excited to deploy this product for any educator looking to quickly get through those end-of-the-year reports.

Quest for the Best Nest

It comes as a shock to nobody that the data shared over the internet, from clients to servers and back again, is mind-bogglingly complex and large. Even within a simple application featuring only a few models, returning different iterations of server side data to the client can result in an unnecessarily intensive CRUD operation. Models feature a variety of different attributes and nested associations that connect in the backend, but not all of these associations need to be displayed to their fullest extent in the user interface. Making fewer and more efficient queries of a program’s database can improve the program’s performance, but which method of returning nested data yields the best results?

With a few simple lines of code, Ruby on Rails and ActiveRecord streamline the process of nesting data, creating associations between models, and querying databases with built-in methods and conventions. The has_many, belongs_to, and has_one macros, for instance, define how models are related to one another and present a library of helpful methods. An example of these associations might be present in a gardening app, which has a user model, a plant model, and plant care instructions model. A user has many plants, a plant belongs to a user and has many care instructions, and an instruction belongs to a plant. A single user would then have many plant care instructions for their entire garden overall. Without Active Record, CRUD operations can be clunkier and leave more room for errors when querying the database:

Without Active Record, the user’s id must be found manually.

With Active Record, the user’s information is reliable and associated with the plant automatically.

However, even with Active Record’s built-in associations, there are still challenges with passing too much data to the user, as well as passing nested data (all of the user’s plant care instructions, for instance). Fortunately, Active Record Serializers and the includes method can assist in a variety of these scenarios.

The includes method is one way to cut down on database querying, and can be used in a model’s controller in tandem with RESTful routing. This purposely versatile and broad method can be used for returning associated data from either the has_many or belongs_to model. The includes method can be used in the plant controller to return the associated user, or in the user controller to return the associated plants. Without Active Record or the includes method, either of these scenarios would have to query the database many times:

SELECT `users`.* FROM `users` ORDER BY `users`.`id`
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 1
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 2
SELECT `plants`.* FROM `plants` WHERE `plants`.`user_id` = 3

Or conversely,

SELECT `plants`.* FROM `plants` ORDER BY `plants`.`id`
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 1
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 2
SELECT `users`.* FROM `user` WHERE `users`.`plant_id` = 3

These queries indicate that associated data must be grabbed one by one. Instead of this, adding the includes method in the controller makes only two requests of the database–one to load the data from plants or users, and one to load the associated data linked by the foreign key.

This query would return all the plants and their associated user.

This query would return all users and their associated garden of plants.

The disadvantage of using the include method is that it requires further tailoring using the only method to avoid returning excessive data to the client. The Controller code quickly becomes messy and overreaching with all of the extra customization.

Fortunately, Rails provides Ruby users with the Active Model Serializer: a specialized class that offers a framework for tailoring and returning the exact data needed by the client. Serializers work with models’ associations and attributes specifically in order to simplify and compartmentalize the controller actions. Using Serializers to return associated or nested data allows Ruby on Rails code to be practical and modular. The Controller and the Serializer can do their jobs cleanly.

Active Model Serializers allow programmers to more easily customize the data they want to access from each model, instead of refining the data in the Controller or the frontend. In the following example, the User Serializer returns only the user’s username, leaving out other model attributes such as the id, password, and any created_at or updated_at timestamps. Using has_many and belongs_to macros, Serializers also automate the return of associated data between models. By bringing in a Plant Serializer, we can link the user and their plants, as well as trim down the information returned from both models.

Serializers can also return customized information through customized routing and methods. For instance, to return specific attributes of plants to the frontend, programmers might create a custom Serializer to return only planting dates:

A route is established in the config file, and then called upon in the plants controller:

Serializers can also handle returning data connected by a has_many through relationship (when the connecting Model features a belongs_to relationship to the two other models) simply by using the has_many macro. 

Unfortunately, Serializers have a failsafe put in place to prevent top models from returning nested data two levels deep (when the connected model features a belongs_to relationship to one model and a has_many relationship to the other). In our case, the User Serializer would be prevented from returning data from the Plant Care Instructions Model. Active Model puts this in place to prevent complexity and errors and improve performance. In a case like this, programmers would indeed have to use a Serializer for the first tier of returned data and an include method in the controller to return the second tier.

There are many considerations to take into account when returning nested data in Rails. The variety of options do require programmers to get specific about their final product, however, that specificity allow for flexibility, scaling, and improved performance when working with so much data.

Sources:

Gusto Engineering Blog

Active Record Guide

Turns out you can't just put your data anywhere...

Gaining exposure to both frontend and backend coding frameworks has shed an embarrassing amount of light on how little I (and what I assume to be many other millions of internet users) actually understand the layers of protocol that make up the internet. I was mostly protected from this realization while learning JavaScript and React. I could perform CRUD operations through an API that would show up immediately on a readily accessible local db.json file lurking in my code editor. As I dove into server-side programming, however, I found myself often wondering where my data was actually going and what the benefits to server-side coding were. As it turns out, working with a web server and working with a local frontend database file are vastly different.

One significant difference between using server-side coding in tandem with a Web Application Framework like Sinatra and working with local database storage is the flexibility, robustness, and specificity of server-side HTTP requests. Working only in client-side code means that all fetch requests must be written and defined alongside user interface objects, navigation, and forms. Not only does compartmentalizing the Request-Response cycle separately from user-facing functionality deliver clean code, but it also means that backend programmers can define customized HTTP responses. Server-side code offers the ability to modify and return finely tailored data. In the example below, the application controller in our Ruby code makes use of previously defined, robust backend database methods to return a variety of responses. These methods, like .by_price, are often built upon huge libraries of useful code, like the .to_json and .first methods. Using client-side code to do the same work would require relying heavily on the few JavaScript and React methods at the developer’s fingertips (map, filter, find, and forEach).

Storing data and sending responses with server-side code is more practical than continuing to work with a frontend database, as well. While my server-side code does run locally, web servers at large are designed to handle multiple requests simultaneously from different users. A local database file will always only be accessible to a single user at a time. Writing code that only utilizes a local database is ultimately simpler, but much less realistic for creating widespread usable programs for users other than me. Local databases do not need to account for race conditions, which is when the outcome of an operation may be interrupted by users. For instance, if two users attempted to request and modify the same bit of data at the same time, or if a user canceled an initial HTTP request before the response was sent. Server-side coding ultimately needs to be powerful enough to account for race conditions with techniques like using locking or queueing mechanisms.

Storing data in the frontend versus the backend produces even more noticeable differences between the way data persists and is accessed. A local database file stores all the working data in a single file that can easily be read and written to. For instance, the programmer can modify a db.json file from within the frontend’s code editor. Those changes will show and persist when the app loads. This can be helpful for seeding data for an app or deleting any mistakes with the data. But with server-side code, the data is more formally structured and kept in a relational table. Because servers are designed for widespread usability, storing data across multiple requests, and in a way that can be accessed by all users of the application, is crucial. Furthermore, while local frontend databases can be useful in storing instances of one object, they do not excel in showing relationships between objects. Because Ruby has access to many powerful database libraries like SQL and ActiveRecord, it is easy to perform CRUD operations on a variety of objects with concise code.

Frontend local databases can be easily read and written to, but lack built in relationships between objects.

Backend or server-side databases use libraries to map out relationships between objects, making CRUD operations easier to perform on instances of many objects.

In conclusion, learning to code in Ruby has been an exciting journey, but it hasn't come without its share of challenges. One of the most significant hurdles I encountered was understanding the differences between a server and a local database file. I had to learn about databases and consider how to store data across multiple requests, which was different from the way I was used to working with data. However, with patience and persistence, I was able to overcome these challenges and build exciting web applications that leverage the full power of Ruby.

https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Web_mechanics/What_is_a_web_server

https://developer.mozilla.org/en-US/docs/Learn/Server-side/First_steps/Introduction

https://www.cs.cornell.edu/courses/cs2110/2016fa/L25-Concurrency2/cs2110Concurrency2-6up.pdf

The Great State

As an emerging developer diving into new languages everyday, I am so excited to have finally grasped the fundamentals of React. There is so much to love! Though JSX is a language made of function calls that return objects and descriptions, I can’t help but feel like a chef throwing in a sprinkle of HTML here, a dash of Javascript there, and seasoning to taste. After learning React, it is hard to imagine a time when we kept these two languages from constantly co-mingling. The flexibility and versatility of JSX allows React apps to respond intuitively to user interaction while allowing developers to instinctively organize their code and spare unnecessary lines of imperative programming.

React’s organization schema has further altered and improved the way that I think about and organize my code within a project. The language’s reliance on components encourages developers to use modular design and functionality, while considering how all parts of the modules interact in the project’s ecosystem. The end result is more accessible, digestible code. Forms belong in one place, cards are rendered in another, and all components are cleanly nested in a parent component. This modularity makes it easier to target and triage errors, add functionality, and build out apps with speed.

Sharing data between these components is naturally built into React. Props represent the different types of data that can be passed between components. However, the magic of React lies in the ways that React apps dynamically update according to changes from user interaction, changes in the database, and changes spurred by DOM events in general. These changes are recorded in a special variable called by useState, a hook and a function that can update, rerender, and ultimately cause changes in the app’s user interface. The useState variable is accessible to parent and child components, can be updated or applied in a number of different ways, and, most importantly, is stored in the app’s memory as the app’s source of truth.

useState is called within the component function. A const of an array with two variables is assigned to useState. The first variable represents the state, and the second variable represents a function that sets the state. In the example below, useState initially sets the greeting variable to “Hello”, which is then rendered as an h1 on the app.

An example of the useState hook, which initially sets our greeting to “Hello.”

A change in state often occurs upon user interaction. Adding a button that has the user change the greeting represents an opportunity to change state. Within the button’s onClick function, setGreeting is called to randomly select a greeting from an array. A button click will result in the value of greeting being changed to a randomly generated greeting.

Button functionality encourages user interaction. The user expects a change to the user interface upon clicking.

In React, a change in state triggers the app’s component to rerender. The user interface is then a function of the newly updated state. But can we achieve the same results with a local variable instead of using state? Is state an unnecessary hook? Rewriting this code with the same functionality but only using local variables would look something like this:

The same functionality, but without using useState.

When running this code, the user interface does not appear to change, even though the greeting variable visibly changes in the console.

Local variables act and change when called on by events as they would in vanilla Javascript. React, however, specifically requires the useState hook, the state variable, and the setter function working in tandem to actually trigger a rerender of the component or entire app. Changing a local variable behind the scenes simply doesn’t give React the tip off to make a visible user interface change. The useState hook urges developers to be intentional with regards to the app’s source of truth–when the state is changed, what the state is, and where the state goes.

So. Are local variables useful for anything in React apps? If a variable is used behind the scenes to store data or as a reference, sure. In fact, if the user interface does not need updating or rerendering when a variable changes, using local variables (and even some other special React hooks) is preferred over useState to avoid triggering unnecessary rerendering and complication. The specificity of useState is one of the many brilliant ways in which React was designed with declarative, intentional code in mind.

Resoures:

https://blog.devgenius.io/whats-and-why-s-of-state-in-react-8208feb6912e

https://reacttraining.com/blog/how-to-use-and-not-use-state/

https://www.geeksforgeeks.org/re-rendering-components-in-reactjs/

https://reactjs.org/docs/hooks-state.html

Asynchronous programming gave me five seconds of my life back...here's how you can get five seconds back too.

Anyone of a certain age remembers the many times that technology put us through tests of enduring patience, particularly in the early, dial-up internet days of the late nineties. These tests of patience–waiting for the mechanical tone-based handshake between your device and the internet service provider–were an early testament to how essential the internet is for all of our lives. Nothing else could create a positive pavlovian response to such a grating atonal noise. Though the internet is obviously now a standard fixture for our everyday lives, our need for a stronger, faster, more efficient and robust system has pushed this technology exactly towards that. Among these advances, asynchronous functions in code have aided in faster loading and a more efficient, sensible way of ordering execution flow within the code.

With synchronous code, Javascript reads and executes each line of code from top to bottom. Each line of code must finish processing before moving on to the next one, as each successive line of code may refer back to previously declared variables, for instance. For shorter code working with smaller datasets, this synchronous process appears nearly instantaneous. Consider the following example of a random wacky name generator:

Although there are functions called within functions, this code is still running synchronously. The generateString function within generateClassroom must be completed before we can push its result to the array, which eventually gives us a string of the wacky students in our classroom.

Running the generateClassroom function with 3, 4, 5, or even 100 or 1,000 students as its argument yields fairly instantaneous results. We start to get into trouble when passing in larger arguments, however. The code takes some time to load with 1,000,000 students, and those of us who lived through the nineties quickly forget our dial-up patience. While making a classroom of 1,000,000 students is somewhat of a farfetched example, it is not uncommon to be working with datasets of that size and much greater.

Asynchronous functions allow us to access, display, and render huge datasets, while the other HTML and simultaneous functions of the overall code can run. This buys time for our impatient 2023 brains, as we can still interact with a dynamic, engaging site without losing up to five precious seconds of our lives. Asynchronous functions operate by first returning to us a Promise object–a receipt that the server has heard us and is working on responding. We can illustrate fetch() requests as an exemplar piece of asynchronous code.

Fetch() is a global method that takes one argument, which is the URL where the datasets are located. Fetch() returns a Promise object representing the server’s response, upon whose results further code can be enacted. Promise objects exist in one of three states: pending, fulfilled, and rejected. Pending represents the initial state of the object, where the request has not yet been fulfilled or rejected; fulfilled promises mean that our request was received, and a response was successfully sent; and rejected promises mean that our operation has failed for any number of reasons. We can view both the overall Promise object, as well as console log the representation of its response:

Once the status of the Promise has been resolved, we can chain a then() method onto our result and process the success or failure of our fetch() promise.

then() methods take a callback function as an argument. Within a simple GET request, our first chained then() method will take the promise’s representation of the server response and oftentimes parse that representation into a JSON format within its callback function. Interestingly, this first then() also returns a Promise object–one that is resolved depending on if the representation was successfully formatted into a JSON object or not. After this second promise, we can finally chain on a second then() to render our JSON object to the DOM, console log our data, or run these data through any other function we wish.

As these asynchronous functions are churning away, the rest of our synchronous code can shine. These two processes used in tandem open up advantages and efficiencies that our 1995 brains wouldn’t even be able to comprehend.

Credits:
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Where the “Coding is a language!” analogy gets disrupted

As a former science, technology, engineering, and math teacher, my favorite part of teaching introductory code to middle school students was likening the subject to language learning. I loved opening that door to students who did not consider themselves “science-y” or “tech-y.” Framing coding as language learning allowed these particular students to see programming as mastering a vernacular pattern and thus wielding a powerful and creative tool. Through human-computer communication, these students could automate art pieces, robots, and a variety of other mechanisms.

Now that I am further along in my programming journey, I can see both the advantages and limitations of this analogy. Most obviously, computers are exacting and do not care for florid creativity. Most challengingly, however, I did not expect programming languages to contain so many self-referential abstractions that have no counterpart whatsoever to human speech. There is no dictionary that describes 501 verbs in Javascript. And thus, I feel as though my battle to understand this is just beginning.

Of course, by this, I mean the Javascript keyword this. Even describing the keyword is confusing! At the current tech company where I work, I see this used in our immense and terrifying codebase over and over again. This is a shapeshifter, morphing to meet the developer’s contextual needs and ultimately providing the parity in code that only a self-referential abstraction could. Despite my intimidation, I am excited to master this powerful and unique piece of language.

Put most simply, this refers to an object, which is essentially a variable or data type with many properties. Without a specific object to refer to, this refers to the global object, which is an object that is always present in the global context of the running code. Oftentimes, the global object represents the window containing the DOM. To give a concrete and frequent example, calling this to refer to a global object will return the tab in which you are running your code:

screenshot of calling this in the global context

This refers to the global object in a few instances:

  1. When this is used alone or as the definition of a variable:

this;

or

let i = this;

i;


  1. When this is used in an unrestricted function that is unattached to an object:

function abc(){

     return this;

}

abc();

On the other hand, this refers to specific objects in Javascript when it is defined within an object’s properties, specifically within a key/value pair that includes a function. This can be paired with the object’s other keys to pull their value.

In the above example, the specs function within house1 calls house1’s other values with this. In this particular function, calling this.bedrooms within specs, for example, would be the same as calling house1.bedrooms outside of the house1 object.

Because pieces of code are more useful when they can be applied broadly to other pieces of code, this comes with a series of methods that allow for its reuse. These methods allow objects to externally “borrow” a function defined in another object without having to define the function in every single object created. In the example above, it is likely that this program would include other house objects. These house objects can be constructed without defining their own method. Using the call() method allows one house object to be passed into the specs function.

The call() method furthermore allows objects to borrow methods defined elsewhere, even if these objects need additional arguments to complete the function. By passing in additional arguments into call(), the versatile combination of this and call() allow methods to be used in a variety of scenarios.

The this suite of methods also contains bind(), which allows programmers to create variables that can be used in different parts of the code. Because this cannot technically be reassigned a value, the bind() method allows a particular variable to be preserved in time and used elsewhere in the code. This variable can be passed as an argument into another function’s parameters, called upon, console logged, and more.

Ultimately, understanding how to program with the this keyword, along with correctly interpreting its meaning when viewing other code will undoubtedly unlock several coding skills. This aids in shrinking the redundancy within code, allows methods to be used beyond the original object to which they are assigned, and is generally essential for object-oriented programming. While computers may not be able to understand concepts of human language like nuance, innuendo, or inflection, I think they certainly have us beat with the utility of this.

Credits:

https://developer.mozilla.org/en-US/docs/Glossary/Global_object

https://www.w3schools.com/js/js_this.asp

https://developer.mozilla.org/en-US/docs/Web/API/Window

https://www.w3schools.com/js/js_objects.asp 

https://www.w3schools.com/js/js_function_bind.asp