# Step-by-Step Guide to Developing a Story Generator with OpenAI's GPT and Ruby on Rails

Last night I was working on a new tool that I'll share more about later. Fans of text based games are going love it when it's ready. :)

In the meantime, I've written this tutorial to discuss how we can set up an OpenAI client in Rails, send it a test prompt, and get a response.

> If you have an OpenAI account, you can find your API keys here:
> 
> [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys)
> 
> If you don't have an OpenAI account yet, make one! I spent a grand total of $0.11 learning this for the first time and setting up this tutorial using text-davinci-003.
> 
> If you are interested in testing out the API, I highly recommend it.

We're going to keep our app super simple for this example's sake, but this can be expanded to be useful in any part of your Rails app.

Let's dive in:

## Step 1: Initialize a new rails app

```bash
rails new new-app
cd new-app
```

## Step 2: Installing the OpenAI gem

Add `ruby-openai` to your gemfile. That was easy. Next.

## Step 3: Set up environment variable

Add `dotenv-rails` to your gemfile and run `bundle install`

Create a new file in your root directory named `.env` and include your OpenAI token. The format is as such, with no quotation marks surrounding the value.

```bash
OPENAI_TOKEN=abcd1234zxcv
```

## Step 4: Add a Character with fields to populate

We're going to make a character model with a name and two traits. We'll ask GPT to write us a backstory for it to easily demonstrate connecting to openAI, prompting it, and updating our record with the response.

Add the character using a scaffold, then run the migration:

```ruby
rails generate scaffold Character name trait_1 trait_2 backstory
rails db:migrate
```

To be clear, in these generators when we describe attributes without describing their type, they're automatically strings.

For simplicity's sake, let's also navigate to `app/views/characters/_form.html.erb`.

Delete the following form labels and text fields since we don't want users to input a backstory or traits, we want the AI to generate them for us.

```xml
  <div>
    <%= form.label :trait_1, style: "display: block" %>
    <%= form.text_field :trait_1 %>
  </div>

  <div>
    <%= form.label :trait_2, style: "display: block" %>
    <%= form.text_field :trait_2 %>
  </div>

  <div>
    <%= form.label :backstory, style: "display: block" %>
    <%= form.text_field :backstory %>
  </div>
```

## Step 5: Set up an example request, see the response

For now lets just throw all this into the character model. If we wanted to extend this out further, we could build a GPT client model with fields for the necessary setup, but we can worry about that in a blog post about best practices or refactoring.

### 5a. GPT client setup

```ruby
# app/models/character.rb
class Character < ApplicationRecord
  require 'openai'
  def gpt_client
    OpenAI::Client.new(access_token: ENV["OPENAI_TOKEN"])
  end

  def gpt_model
    "text-davinci-003"
  end

  def run_prompt(attribute, prompt, max_tokens)
    response = gpt_client.completions(
      parameters: {
        model: gpt_model,
        prompt: prompt,
        max_tokens: max_tokens
      }
    )
    self.update(attribute => response["choices"][0]["text"])
  end
end
```

Lets break this down piece by piece.

First off, we're requiring `openai` so that we have access to the gem's library. That's how we're able to utilize it in the `gpt_client` method.

Next is our `gpt_client` method, which establishes a connection with OpenAI. `ENV['OPENAI_TOKEN']` is how we access environment variables, stored by dotenv in our `.env` file we made earlier.

Self-explanatorily, `gpt_model` is the name of the model we want to use. You can use GPT-4 if you want, but it's more expensive. Check out API pricing [here](https://openai.com/api/pricing).

Now we get to the magic. The `run_prompt` method is designed in such a way that we can update `self`, in this case our Character, by invoking `run_prompt` with an attribute, prompt, and the maximum amount of tokens we'd be willing to spend on the query. Token pricing calculator, separate from API pricing, is available [here](https://platform.openai.com/tokenizer).

An example of invoking this might be as such:

`Character.first.run_prompt(:trait_1, "Give me a number from 20-100", 100)`

After running this in your terminal you would see the SQL as your Character model is updated with a new 'age' value. If you refreshed your character's page, you'd see that reflected in the UI.

### 5b. Adding prompts

Now that we've got a connection to OpenAI working, it's time to start chaining methods together to build out a backstory for our character. Lets add some prompts into the model:

```ruby

  def generate_all
    self.generate_traits
    self.generate_backstory
  end

  def generate_traits
    run_prompt(:trait_1, trait_1_prompt, 60)
    run_prompt(:trait_2, trait_2_prompt, 60)
  end

  def generate_backstory
    run_prompt(:backstory, backstory_prompt, 500)
  end

  def trait_1_prompt
    "Describe a positive trait about #{self.input_name}. Only use one word"
  end

  def trait_2_prompt
    "Describe a negative trait about #{self.input_name}. Only use one word"
  end

  def backstory_prompt
    "You're a legendary storycrafter. Build a backstory about a fantasy character named #{self.name}. They have #{self.trait_1} and #{self.trait_2} traits."
  end
```

In this code we're setting up prompts to send to GPT. If you've ever typed into the chat interface for ChatGPT, these prompts are exactly the same as a prompt you might type there.

> Protip: you can tell GPT to take the role of an experienced author from history if you're interested in adding creative flair.

As you can see with the way we utilize `#{self.name}` in `backstory_prompt`, you can combine user entered terms with your own prompts in order to allow users to write inputs for the command. In this case, we're using AI generated traits, but a user could provide them as well.

Also noteworthy, in each instance of us running `run_prompt`, we are changing the amount of `max_tokens` we use. This is to prevent costs from being ridiculously high. Since we only want one word for these traits, 60 tokens is more than enough. But if the `max_tokens` was still set to `500`, we may end up using more than we expect.

### 5c. Call our model's method

Lets call `generate_all` to generate AI characteristics when we save a character in the `characters_controller.rb`:

```ruby
#app/controllers/characters_controller.rb
  def create
    @character = Character.new(character_params)

    respond_to do |format|
      if @character.save
        @character.generate_all
        format.html { redirect_to @character, notice: "Character was successfully created." }
      end
    end
  end
```

Now, when you create a new character at the `/characters/new` route, after the page loads you will get an AI generated backstory!

## Step 6: Expanding

There's a lot of flaws in this implementation. I just wanted to build a quick example of how easy it was to get set up with OpenAI and start implementing results from LLMs into your own applications!

If you were to build this out, I would recommend building out a new model for the GPT connection and for prompts to separate concerns out of the Character model and keep logic clean.

You could also move the AI queries to a background worker like [Delayed Job](https://github.com/collectiveidea/delayed_job) or [Sidekiq](https://github.com/sidekiq/sidekiq) in order to create a smoother experience for the user. Alternatively, a loading screen with a progress bar would be nice.

Of course, projects like these always look better styled in a UI.

## Final thoughts, useful links

Thanks so much for reading this all the way through!

If you run into any issues implementing this, feel free to leave a comment and I'd be happy to connect and help you troubleshoot.

**Links**

Check out this repo on github: [https://github.com/frogr/ruby-AI-blog-post](https://github.com/frogr/ruby-AI-blog-post/tree/main)

Follow me on twitter: [https://twitter.com/ex\_austin](https://twitter.com/ex_austin)

Subscribe to receive more posts like this straight into your inbox: [https://austn.net/newsletter](https://austn.net/newsletter)
