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 feedmax_tokens
- approximately the max amount of words we want returnedtemperature
- 1.0 means creative whereas 0 means a well defined answerstop
- 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
And this is lib/recipe_story.rb
:
require_relative "recipe_base"
class RecipeStory < RecipeBase
PROMPT_KEY = "recipe_story"
def generate
@_generate ||= openai.completion(
prompt: prompt,
max_tokens: 512,
temperature: 0.8,
stop: "Name:"
).dig("choices").first.dig("text")
end
end
Great! Now let's go back into IRB via the terminal and see the backstory of "Potato Jello".
$ irb
irb(main):001:0> load "lib/recipe_story.rb"
=> true
irb(main):002:0> story = RecipeStory.new(name: "Potato Jello")
irb(main):003:0> puts story.generate
Potato Jello. When I was a kid growing up in the midwest during the 70's, my mom always served
this for Thanksgiving. I remember all the little kids in the family asking what it was and if
they could try it, and of course, the answer was NO. Fear of this food runs deep in my family,
as it does with many families from the Midwest. This potato jello being served for Thanksgiving
on the table with all the other fancy food was always the biggest joke in our family. I think
about it now and laugh because we were probably very lucky to get even a bite of it.
This recipe for Potato Jello has been passed down through the generations in my family. I don't
even remember where my mom got it from, but I always make it for Thanksgiving, and my kids get a
kick out of it to this day. It's just so dang good. It has the consistency of jello, but it's
somehow still a creamy potato salad. It's full of potato chunks, celery, and onions in a mayo base.
I'll be making this for Thanksgiving this year, same as I have for the last 20 years, and it will
probably be the same for the next 20. It's just a part of the holiday tradition for our family.
This recipe is certainly a keeper, and it's one you should try out for your family sometime!
This is a fun recipe for all ages. The kids will love the colors, and everyone will love the
delicious flavor.
=> nil
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.