Generating Cooking Recipes with OpenAI's GPT-3 and Ruby

August 28, 2020
Written by
Drew Baumann
Contributor
Opinions expressed by Twilio contributors are their own

Generating Cooking Recipes with OpenAI's GPT-3 and Ruby

I enjoy cooking. When I decide to make a new recipe I typically go hunting for that perfect variation of a dish. Often this leads to food blogs. One of the things about food blogs that I find interesting is the need for the author to write a lengthy backstory on why a given recipe means so much to them. The enthusiasm for their recipes gave me the inspiration to make parody recipes using that same voice. The twist: let artificial intelligence do the heavy lifting.

Sloppy Joe Waffles:

What’s better than a sloppy joe? Sloppy joe waffles! These sloppy joe waffles are so so good! They’re almost like a cross between a sloppy joe and a classic waffle. They’re warm, hearty, and just all around delicious food meets breakfast food.

I’m a bit obsessed with the idea of breakfast for dinner. I love to make breakfast foods and dinner foods. They’re just so good. Spanakopita and breakfast tacos are among my favorite things to make for dinner. Like those, these sloppy joe waffles are complete comfort food. They’re warm, hearty, and so so good.

The possibilities for these sloppy joe waffles are endless. If you’re not a fan of sloppy joes, you can always make the waffles without the sauce and it’ll be just as delicious. No worries if you don’t have time to make the sloppy joe sauce. You can just serve the sloppy joe waffles with ketchup or hot sauce. They’re simple and delicious no matter what.

Ingredients:

Sloppy Joe filling, waffles, cheese, butter, green onions, ketchup, mustard

Recipe:

1. Heat waffle iron.

2. Spread a generous amount of ketchup and mustard onto the waffle.

3. Add a layer of cheese.

4. Add a layer of Sloppy Joe filling.

5. Add a layer of green onions.

6. Top with another layer of cheese.

7. Top with another layer of Sloppy Joe filling.

8. Top with another layer of green onions.

9. Close the waffle iron and cook until the cheese is melted and the waffle is golden brown.

10. Serve with a side of extra ketchup and mustard.

What is GPT-3?

I generated that story and recipe using the latest and greatest in AI technology: the GPT-3 API from OpenAI. I’ll show you how to build a simple app to create recipes and food journals just like this one. GPT-3 (Generative Pre-trained Transformer 3) is a model that was trained on a very large amount of text. In other words, it knows how people write and is very good at picking up written patterns based on user entry. It is surprisingly easy to use: you feed it some text and the model generates more text following the existing pattern and structure.

In high school English class one day I had access to the department computer lab to work on a poem writing assignment. After completing the task I goofed around online by generating new “poems”.  This was done by writing sentences, throwing them through multiple translation sites, and seeing how “poetic” the mangled lines became. With GPT-3 I can generate poems with considerably less effort.

To get started we feed the GPT-3 API a prompt:

Title: The Rockstar's Dilemma

I want to sing
I want to scream
I want to be heard
but I'm all alone

Title: Cornflakes for Breakfast

a crunchy start to my day
honey on top of my cornflakes
I'm in a good mood

Title: TV Dinner Date

Mommy's not home
it's just me and the TV
I'm all alone
but why am I so happy?

Title: Goose on the Loose

The AI detected the pattern that I supplied: a title, followed by a poem.  By supplying a title at the end of the prompt, the API generated the poem based on that title, which is referred to as the “completion”.

A goose has flown
It's wings flap in the air
It's flying free

In fact, all the poems provided for this example prompt were in turn created by GPT-3 based on real poems given as a prompt. How cool is that?

GPT-3 is non-deterministic, so you can supply the same prompt multiple times and get unique output each time. Here are a couple alternative versions of “Goose on the Loose”. Note that each generated poem is completely different from one another:

I feel like a goose
out of control
flying higher and higher
but it's not a bad thing
I don't know where I am
but I'm out of my cage
I'm not happy
I'm not sad
I just feel like dancing

The pattern detection isn’t perfect. I initially set out to make haikus, but it failed to consistently create the 5-7-5 syllable pattern. It won’t be the last time technology has let me down.

Now that you understand how GPT-3 works let's get building.

Tutorial Requirements

To follow this tutorial you need to have the following:

  • Ruby 2.0.0 or higher. To install refer to the instructions on ruby-lang.org.
  • I recommend using a manager such as rbenv to handle your installation. This will allow for a user level install and you can avoid using sudo!
  • An OpenAI API key. Request beta access here.

Creating Recipes with GPT-3 and Ruby

In this tutorial we will build an application that takes just a recipe name and generates a backstory and recipe.

Setup

First things first, let's create a new directory for our project and set up some dependencies.

If you are using a Unix or Mac OS system, open a terminal and enter the following commands to do the tasks described above:

$ mkdir gpt3-recipe-builder
$ cd gpt3-recipe-builder
$ mkdir lib
$ gem install bundler

If you are using Windows, enter the following commands in a command prompt window:

$ md gpt3-recipe-builder
$ cd gpt3-recipe-builder
$ md lib
$ gem install bundler

Open up your favorite code editor and create the file Gemfile:

source "https://rubygems.org"

gem "dotenv"
gem "http"
gem "titleize"
gem "tty-prompt"

Save and go back to your terminal to install those gems.

$ bundle install

Now create a .env file and add in your OpenAI API key like so:

OPENAI_KEY=your-openai-api-key-here

Create a Class to Communicate With OpenAI

Since OpenAI doesn’t have a Ruby client we are going to build a class to support our efforts.

To do that lets first create a new file named openai.rb and put it in a new lib directory.

With the editor of your choosing, open the newly created file and insert the code below.

require "http"

class OpenAI
  URI = "https://api.openai.com/v1"

  def initialize(api_key:)
    @api_key = api_key
  end

  def completion(prompt:, max_tokens: 64, temperature: 1.0, stop: "<|endoftext|>")
    response = HTTP.headers(headers).post(
      "#{URI}/engines/davinci/completions",
      json: {
        prompt: prompt,
        max_tokens: max_tokens,
        temperature: temperature,
        stop: stop
      }
    )
    response.parse
  end

  private

  attr_reader :api_key

  def headers
    {
      "Content-Type" => "application/json",
      "Authorization" => "Bearer #{api_key}"
    }
  end
end

The OpenAI endpoint takes a number of parameters, but we are focused on four for our method:

  • prompt - the text we feed
  • max_tokens - approximately the max amount of words we want returned
  • temperature - 1.0 means creative whereas 0 means a well defined answer
  • stop - where we want the completion to stop generating text

Now that we have a class set up to interface with the OpenAI API lets create a class to build a recipe based on ingredients!

Create a Class to Generate a Recipe

Again open up your editor and create a new file in lib named recipe.rb

Now that we have created the file we want to build a method to generate a recipe based on a recipe name. We are going to be doing a few different things here let’s take this step by step.

require "dotenv/load"
require "yaml"
require_relative "openai"

class Recipe
  PROMPT_FILE = "prompts.yml"
  PROMPT_KEY = "recipe"

  def initialize(name:)
    @name = name
  end

  def generate
    @_generate ||= openai.completion(
      prompt: recipe_prompt,
      max_tokens: 512,
      temperature: 0.4,
      stop: "Name:"
    ).dig("choices").first.dig("text")
  end

  private

  attr_reader :name

  def openai
    OpenAI.new(api_key: ENV["OPENAI_KEY"])
  end

  def recipe_prompt
    YAML.load_file(PROMPT_FILE).dig(PROMPT_KEY) + name + "\n"
  end
end

Now we can create a method to actually generate the recipe. I am using memoization to store the result of our query to the OpenAI completion API endpoint. As mentioned earlier, “completions” are non-deterministic so if we want to reference this data again we need to store it.

We are setting the temperature to 0.4 as we want the recipe to be somewhat creative, but more deterministic based on our input. max_tokens should be set high so we can ensure a complete recipe. To ensure that the “completion” doesn’t continue to generate new recipes we need to tell it to stop at “Name:”. That will ensure that we only get the body of the recipe. Finally we need to supply a prompt.

The prompts for this project are quite large so I decided to store them in a yaml file. I could have stored the text as a text file, but because we will have two prompts I decided a yaml file was more fitting for this project. One prompt for our backstory and another for our recipe. As you can see in the recipe_prompt method we are loading a prompt file and then accessing the prompt by its key. We are also adding our recipe name and a new line to match the pattern of the prompt.

Before we try this out we need to create our prompts.yml file.

recipe: |
  Name: Sugar Cookies

  Ingredients:
  flour, salt, baking soda, butter, sugar, egg, milk, vanilla extract

  Recipe:
  1. Whisk flour, salt, and baking soda together in a bowl. In a separate bowl, cream the butter, white sugar, and brown sugar together until mixture is light and fluffy, 3 to 4 minutes. Add the egg, milk, and vanilla extract. Whisk liquids together in small areas around the bowl, then all together to avoid separation.

  2. Pour dry ingredients into the wet ingredients; stir until flour is thoroughly mixed in. Stir in the chocolate chips.

  3. Transfer dough to a resealable plastic bag. Refrigerate until dough is firm, at least 2 hours.

  4. Preheat oven to 375 degrees F (190 degrees C). Line baking sheet with parchment paper.

  5. Scoop out rounded tablespoons of dough and place on prepared baking sheet, leaving 4 inches of space between cookies (about 8 per sheet). Bake in preheated oven until cookies are golden brown, about 12 minutes. Slide parchment and cookies onto a cooling rack for a few minutes. Remove parchment and finish cooling the cookies on the rack.

  Name: Shrimp Scampi

  Ingredients:
  butter, shrimp, olive oil, pepper, salt, shallots, linguine, red pepper flakes, garlic, shallots

  Recipe:
  1. Bring a large pot of salted water to a boil; cook linguine in boiling water until nearly tender, 6 to 8 minutes. Drain.

  2. Melt 2 tablespoons butter with 2 tablespoons olive oil in a large skillet over medium heat. Cook and stir shallots, garlic, and red pepper flakes in the hot butter and oil until shallots are translucent, 3 to 4 minutes. Season shrimp with kosher salt and black pepper; add to the skillet and cook until pink, stirring occasionally, 2 to 3 minutes. Remove shrimp from skillet and keep warm.

  3. Pour white wine and lemon juice into skillet and bring to a boil while scraping the browned bits of food off of the bottom of the skillet with a wooden spoon. Melt 2 tablespoons butter in skillet, stir 2 tablespoons olive oil into butter mixture, and bring to a simmer. Toss linguine, shrimp, and parsley in the butter mixture until coated; season with salt and black pepper. Drizzle with 1 teaspoon olive oil to serve.

  Name:
recipe_story: |
  Name: Cheesy Potato Casserole

  Do you have those family recipes that you make every holiday season without fail? In our family, we have a few, and they're what I call super "old school" recipes. These are the classics that my brothers love more than anything. And they're the ones we've been eating since I was a kid. You have those recipes too, right?

  We always do a cheesy potato casserole at Christmas. But a few years ago I decided I wanted to make something new and updated. I knew I had to keep the flavors pretty simple and classic. I also knew I wanted an easy side dish that I could prepare ahead of time and not have to worry about. Enter this casserole. It's easy to throw together, but so incredibly delicious, and it's pretty much guaranteed to be loved by all.

  Cheesy potatoes are one of those old school recipes. They're without a doubt a tried and true favorite. Meaning they're never not delicious, everyone who tries them loves them. I'm sure many of you have similar casseroles in your family. I know some call them funeral potatoes (which seems really depressing to me). Call um what you please, but in our house, they're just cheesy potatoes.

  My mom's recipe called for frozen potatoes, canned cream of chicken soup, sour cream, butter, cheese, and a few other ingredients. I decided to take her recipe and give it my own spin, updating a classic, and making it BETTER. I'd actually been wanting to do this for years but feared I wouldn't be able to create something better.

  But a few years back I decided I'd had enough with the canned soup (because it freaks me out and I will not eat it...so yes, I used to miss out on cheesy potatoes every year). I knew it was time to update the cheesy potatoes recipe. Thankfully the new version turned how better than I could have imagined. This casserole is SO GOOD.

  Name: French Bread

  If you're looking for an easy french bread recipe, this is it! You'll have 2 beautiful loaves in 90 minutes. The actual time you'll be working will only be about 15 minutes, the rest of the time will be patiently waiting for the dough to rest/rise. This homemade french bread is golden and crispy on the outside, while remaining soft and slightly chewy on the inside. We love to serve it with a bowl of warm soup and the first time I made it, my husband said "this bread is SO good, where did you get it?" He thought I picked it up from our local bakery. It is just that good and really so easy to make. You'll never want to buy store bought french bread again.

  I absolutely love making bread and rolls of all kinds. There's something so satisfying about making them in your own kitchen. The smell is unbelievable. We love to serve this homemade french bread with softened butter, honey and/or jam. So so good. Trust me when I say, this is the best french bread recipe!

  Name:

Now that we have our YAML file with our prompts lets test the method in IRB via the terminal.

$ irb
irb(main):001:0> load "lib/recipe.rb"
=> true
irb(main):002:0> recipe = Recipe.new(name: "Potato Jello")
irb(main):003:0> puts recipe.generate

Ingredients:
potatoes, salt, water, gelatin, cream of chicken soup, cream of mushroom soup, sour cream, milk, butter, onion, pepper, garlic powder, salt, pepper

Recipe:
1. Boil potatoes until tender. Drain and mash.
2. Add cream of chicken soup, cream of mushroom soup, sour cream, milk, butter, onion, pepper, garlic powder, salt, and pepper.
3. Add 1 package of gelatin.
4. Mix until well blended.
5. Pour into a 9x13 pan.
6. Refrigerate until set.
7. Cut into squares.
=> nil

Delicious. Next we will craft a story for this recipe.

Create a Class to Generate a Recipe Story

The great news here is that this class will look and behave much like our Recipe class.

Open up your editor and create recipe_story.rb in the lib directory.

Now add the following code:

require "dotenv/load"
require "yaml"
require_relative "openai"

class RecipeStory
  PROMPT_FILE = "prompts.yml"
  PROMPT_KEY = "recipe_story"

  def initialize(name:)
    @name = name
  end

  def generate
    @_generate ||= openai.completion(
      prompt: prompt,
      max_tokens: 512,
      temperature: 0.8,
      stop: "Name:"
    ).dig("choices").first.dig("text")
  end

  private

  attr_reader :name

  def openai
    OpenAI.new(api_key: ENV["OPENAI_KEY"])
  end

  def recipe_prompt
    YAML.load_file(PROMPT_FILE).dig(PROMPT_KEY) + name + "\n"
  end
end

This time around I decided to make the temperature 0.8. This allows the completion to be more creative, but still ensuring I get the general style I am aiming for.

As you can see there is some duplication which presents a great opportunity to clean up our code.

Create a base class in lib called recipe_base.rb that our recipe classes can inherit from:

Extract all the common code between our two classes:

require "dotenv/load"
require "yaml"
require_relative "openai"

class RecipeBase
  PROMPT_FILE = "prompts.yml"

  def initialize(name:)
    @name = name
  end

  private

  attr_reader :name

  def openai
    OpenAI.new(api_key: ENV["OPENAI_KEY"])
  end

  def prompt
    YAML.load_file(PROMPT_FILE).dig(self.class::PROMPT_KEY) + name + "\n"
  end
end

Finally lets update our recipe classes to utilize the base class and remove duplicate code. Here is the updated lib/recipe.rb:

require_relative "recipe_base"

class Recipe < RecipeBase
  PROMPT_KEY = "recipe"

  def generate
    @_generate ||= openai.completion(
      prompt: prompt,
      max_tokens: 512,
      temperature: 0.4,
      stop: "Name:"
    ).dig("choices").first.dig("text")
  end
end

Tying it Together With a User Interface

It is all coming together now. We have our class to interface with the OpenAI API, we have our builder classes which output recipes and stories, and now we need to create a simple user interface for generating some delicious meals.

Time to go back to your editor and create recipe_generator.rb.

require "titleize"
require "tty-prompt"
require_relative "lib/recipe"
require_relative "lib/recipe_story"

class RecipeGenerator
  def initialize(recipe_name:)
    @recipe_name = recipe_name.titleize
  end

  def self.run
    prompt = TTY::Prompt.new
    answer = prompt.ask("What do you want to make?", required: true)
    recipe = new(recipe_name: answer)
    recipe.display
  end

  def display
    puts "Recipe: #{recipe_name}"
    puts recipe_story
    puts recipe
  end

  private

  attr_reader :recipe_name

  def recipe
    Recipe.new(name: recipe_name).generate
  end

  def recipe_story
    RecipeStory.new(name: recipe_name).generate
  end
end

RecipeGenerator.run

In this class we are pulling in two new libraries: tty-prompt and titleize. tty-prompt will allow the user to enter their recipe name in the prompt. titleize ensures that the recipe name is properly capitalized for our input.

Outside of our class we are going to call RecipeGenerator.run. Doing so will allow us to run our generator simply by opening the file with ruby. Inside of the run class method we are setting up the prompt, getting the user input, creating an instance of RecipeGenerator, and displaying the recipe to the user.

Time to try it out! In terminal enter:

$ ruby recipe_generator.rb
$ ruby recipe_generator.rb
What do you want to make? Deep Fried Apples
Recipe: Deep Fried Apples

Have you ever seen those deep fried apples at Cracker Barrel? I've been dying to make them for
years now. I'm finally getting around to it, and they are SO delicious!! I used fresh apples,
but if you're looking for a shortcut, you can use canned sliced apples. I use a deep fryer to
make these. If you don't have a deep fryer, you could make them in a pan with oil and the same
method. However, keep in mind that you will have to adjust the temperature, because you can't be
monitoring the oil temperature like you can with a deep fryer. The homemade apples are much
better, but the quick and easy alternative is great too.

I love to serve these deep fried apples with a scoop of ice cream or homemade whipped cream on
top. So good! Since these apples are fried, they are super sweet, the perfect topping for a scoop
of vanilla ice cream! I love having this for a special dessert, like Thanksgiving, Christmas or
for our family Christmas Eve get together with friends.


Ingredients:

2 cups flour

1/2 cup sugar

2 teaspoons cinnamon

1/2 teaspoon nutmeg

1/2 teaspoon salt

1 egg

1/2 cup milk

2 apples

oil for frying

Recipe:

1. Mix together flour, sugar, cinnamon, nutmeg, and salt. Beat egg and milk together in a small
bowl.

2. Cut apples in half and scoop out the core. Cut each half into 6 wedges. Dip each wedge into the
egg mixture, then roll in the flour mixture.

3. Heat oil in a deep fryer to 375 degrees F. Fry the apples in the hot oil until golden brown,
about 2 minutes. Drain on paper towels.

Conclusion

While the last few months have been primarily monotonous through the pandemic, I want to thank Twilio and OpenAI for giving me the opportunity to explore this amazing technology. I had a ton of fun building this tutorial and learning about OpenAI’s GPT-3 API, and I hope that you did as well. If Ruby is not a language in your toolbelt, I hope you enjoyed seeing how it can be used.

If you liked this project, check out my blog https://soullessfood.com/, in which I post made-up recipes I generated. And if you want to use it as a resource, the full source code can be found on my GitHub.

This is a small example showcasing the power of GPT-3 and I hope you get the opportunity to build something amazing with it. If you do send me a message at @drewbaumann on Twitter.

Drew Baumann is the CTO of Repeat (We are hiring!). If you want to work with him on fun projects reach out to drewbaumann [at] gmail [dot] com.