A Ruby gem that makes it easy to use OpenAI's generative AI models. Designed for learners: conversations are just arrays of hashes, so you can see exactly what's happening at every step.
-
Add to your Gemfile and install:
gem "ai-chat", "< 1.0.0"
bundle install -
Set up your API key in a
.envfile at the root of your project:AICHAT_PROXY=true AICHAT_PROXY_KEY=your-key-from-prepend-me(If you have your own OpenAI account, you can skip proxy mode and set
OPENAI_API_KEYinstead.) -
Use it:
require "dotenv/load" require "ai-chat" chat = AI::Chat.new chat.user("What is Ruby?") response = chat.generate! ap response
That's it. generate! returns the assistant's reply as a Hash, and chat.messages holds the full conversation as an Array of Hashes you can inspect, loop through, or store in a database.
Every conversation with an AI model is an array of hashes. Each hash has two keys:
:role-- who's speaking ("system","user", or"assistant"):content-- what they said
Here's what a conversation looks like:
chat = AI::Chat.new
chat.user("If Ruby had an official motto, what might it be?")
response = chat.generate!
ap response
# => {
# :role => "assistant",
# :content => "Matz is nice and so we are nice.",
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
# }
ap chat.messages
# => [
# {
# :role => "user",
# :content => "If Ruby had an official motto, what might it be?"
# },
# {
# :role => "assistant",
# :content => "Matz is nice and so we are nice.",
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
# }
# ]generate! returns the assistant's message as a Hash. The :response key holds metadata from the API (token usage, response ID, model used, etc.). The user and system hashes are just :role and :content.
This design is intentional:
- You can see what you're building.
ap chat.messagesat any point shows the exact data structure. - It reinforces Ruby fundamentals. Arrays, hashes, symbols -- you already know these.
- It's flexible. The same structure works when loading messages from a database:
chat = AI::Chat.new
chat.messages = @conversation.messages # Load from your database
chat.user("What should I do next?")
chat.generate!The user method adds a message with role: "user" and generate! sends the conversation to the API and returns the assistant's reply:
chat = AI::Chat.new
chat.user("Hello!")
ap chat.generate!
# Continue the conversation
chat.user("What about Rails?")
ap chat.generate!You can also add system instructions (to guide the model's behavior) and manually add assistant messages (to reconstruct past conversations):
chat = AI::Chat.new
chat.system("You are a helpful assistant that talks like Shakespeare.")
chat.user("What is Ruby?")
chat.generate!Under the hood, these are shortcuts for the add method:
# These are equivalent:
chat.system("You are helpful")
chat.add("You are helpful", role: "system")
# These are equivalent:
chat.user("Hello!")
chat.add("Hello!") # role defaults to "user"
# These are equivalent:
chat.assistant("Here's what I think...")
chat.add("Here's what I think...", role: "assistant")The gem defaults to gpt-5.2. You can change it:
chat = AI::Chat.new
chat.model = "gpt-4o"By default, the gem looks for an environment variable based on whether proxy mode is on or off:
| Mode | Environment variable |
|---|---|
Proxy on (AICHAT_PROXY=true) |
AICHAT_PROXY_KEY |
| Proxy off (default) | OPENAI_API_KEY |
You can also specify a custom environment variable name or pass the key directly:
# Use a different environment variable
chat = AI::Chat.new(api_key_env_var: "MY_OPENAI_TOKEN")
# Or pass the key directly
chat = AI::Chat.new(api_key: "sk-...")If you're using a Prepend.me proxy key (common in classroom settings), add these to your .env file:
AICHAT_PROXY=true
AICHAT_PROXY_KEY=your-key-from-prepend-me
You can also enable proxy mode in code:
# At construction time
chat = AI::Chat.new(proxy: true)
# Or toggle it on an existing instance
chat = AI::Chat.new
chat.proxy = trueWhen proxy is enabled, API calls are routed through Prepend.me, and the gem uses AICHAT_PROXY_KEY instead of OPENAI_API_KEY.
Give the model access to current information from the internet:
chat = AI::Chat.new
chat.web_search = true
chat.user("What are the latest developments in the Ruby language?")
chat.generate!Use the image: or images: parameter to send images along with your message:
chat = AI::Chat.new
# Single image
chat.user("What's in this image?", image: "photo.jpg")
chat.generate!
# Multiple images
chat.user("Compare these", images: ["image1.jpg", "image2.jpg"])
chat.generate!You can pass local file paths, URLs (https://...), or file-like objects (such as File.open(...) or Rails uploaded files).
Use the file: or files: parameter to send documents:
chat = AI::Chat.new
# Single file
chat.user("Summarize this document", file: "report.pdf")
chat.generate!
# Multiple files
chat.user("Compare these", files: ["doc1.pdf", "doc2.txt"])
chat.generate!PDFs are sent as attachments. Text-based files have their content extracted and sent as text.
You can combine images and files in one message:
chat.user("Analyze these materials",
images: ["chart1.png", "chart2.png"],
files: ["report.pdf", "data.csv"])
chat.generate!Instead of getting back a plain text response, you can ask the model to return data in a specific shape by setting a JSON schema:
chat = AI::Chat.new
chat.system("You are an expert nutritionist. Estimate the nutritional content of the meal the user describes.")
chat.schema = {
type: "object",
properties: {
fat: { type: "number", description: "Fat in grams" },
protein: { type: "number", description: "Protein in grams" },
carbs: { type: "number", description: "Carbohydrates in grams" },
calories: { type: "number", description: "Total calories" }
},
required: ["fat", "protein", "carbs", "calories"],
additionalProperties: false
}
chat.user("1 slice of pizza")
response = chat.generate!
data = response[:content]
# => { fat: 15, protein: 12, carbs: 35, calories: 285 }
data[:calories] # => 285When a schema is set, generate! returns a parsed Ruby Hash with symbolized keys instead of a String.
The gem accepts several schema formats and automatically wraps them for the API. You can also pass schemas as JSON strings. See the examples/ directory for all supported formats.
You can use AI to generate a schema from a plain English description:
AI::Chat.generate_schema!("A user profile with name (required), email (required), age (number), and bio (optional).")This returns the JSON schema as a String and saves it to schema.json. Pass location: false to skip saving, or location: "path/to/file.json" to save elsewhere.
Enable OpenAI's image generation tool to create images from descriptions:
chat = AI::Chat.new
chat.image_generation = true
chat.user("Draw a picture of a kitten")
chat.generate!Generated images are saved to ./images by default (in timestamped subfolders like ./images/20250804T113039_resp_abc123/001.png). You can change the folder:
chat.image_folder = "./my_images"The assistant's message will include an :images key with the saved file paths:
chat.last[:images]
# => ["./images/20250804T113039_resp_abc123/001.png"]AI-generated images are stored by OpenAI, so you can refine them in follow-up messages without re-sending:
chat.user("Make it even cuter")
chat.generate!Enable the code interpreter to let the model write and execute Python code on OpenAI's servers. This is useful for math, data analysis, and generating charts:
chat = AI::Chat.new
chat.code_interpreter = true
chat.user("Plot y = 2x^3 for x from -5 to 5")
chat.generate!The model will write a Python script, execute it, and return the result (including any generated files like charts).
You can look at the conversation at any point:
chat = AI::Chat.new
chat.system("You are a helpful cooking assistant")
chat.user("How do I boil an egg?")
response = chat.generate!
# The return value is the assistant's reply
response[:content]
# => "Here's how to boil an egg..."
# See the whole conversation
ap chat.messagesYou can manually build up a conversation without calling the API, which is useful for reconstructing a past conversation from your database:
chat = AI::Chat.new
chat.system("You are a helpful assistant who provides information about planets.")
chat.user("Tell me about Mars.")
chat.assistant("Mars is the fourth planet from the Sun....")
chat.user("What's the atmosphere like?")
chat.assistant("Mars has a very thin atmosphere compared to Earth....")
# Now continue with an API-generated response
chat.user("Are there any current missions?")
chat.generate!You can also set all messages at once with an array of hashes:
chat = AI::Chat.new
chat.messages = [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Tell me about Mars." },
{ role: "assistant", content: "Mars is the fourth planet from the Sun...." },
{ role: "user", content: "What's the atmosphere like?" },
{ role: "assistant", content: "Mars has a very thin atmosphere...." }
]
chat.user("Could it support human life?")
chat.generate!For messages with images or files, use chat.user(..., image:, file:) instead so the gem can build the correct multimodal structure.
Control how much reasoning the model does before responding:
chat = AI::Chat.new
chat.reasoning_effort = "high" # "low", "medium", or "high"
chat.user("Explain the tradeoffs between microservices and monoliths.")
chat.generate!By default, reasoning_effort is nil (no reasoning parameter is sent). For gpt-5.2, this is equivalent to no reasoning.
Control how concise or thorough the model's response is:
chat = AI::Chat.new
chat.verbosity = :low # :low, :medium, or :highLow verbosity is good for short answers and simple code generation. High verbosity is better for thorough explanations and detailed analysis. Defaults to :medium.
Start a response and poll for it later:
chat = AI::Chat.new
chat.background = true
chat.user("Write a detailed analysis of Ruby's GC implementation.")
chat.generate!
# Poll until it completes
message = chat.get_response(wait: true, timeout: 600)
puts message[:content]The gem automatically creates a server-side conversation on your first generate! call:
chat = AI::Chat.new
chat.user("Hello")
chat.generate!
chat.conversation_id # => "conv_abc123..."
# The model remembers context across messages
chat.user("What did I just say?")
chat.generate!You can load an existing conversation:
chat = AI::Chat.new
chat.conversation_id = @thread.conversation_id # From your database
chat.user("Continue our discussion")
chat.generate!Each assistant message includes an API response hash with metadata:
chat = AI::Chat.new
chat.user("Hello!")
chat.generate!
response = chat.last[:response]
response[:id] # => "resp_abc123..."
response[:model] # => "gpt-5.2"
response[:usage] # => { input_tokens: 5, output_tokens: 7, total_tokens: 12 }The last_response_id reader always holds the most recent response ID:
chat.last_response_id # => "resp_abc123..."The get_items method fetches all conversation items from the API, including messages, tool calls, reasoning steps, and web searches:
chat = AI::Chat.new
chat.reasoning_effort = "high"
chat.web_search = true
chat.user("Search for Ruby tutorials")
chat.generate!
# Pretty-printed in IRB/console
chat.get_items
# Iterate programmatically
chat.get_items.data.each do |item|
case item.type
when :message
puts "#{item.role}: #{item.content.first.text}"
when :web_search_call
puts "Searched: #{item.action.query}" if item.action.respond_to?(:query)
when :reasoning
puts "Reasoning: #{item.summary.first.text}" if item.summary&.first
end
endAll display objects have a to_html method for rendering in ERB templates:
<%= @chat.to_html %>
<%= @chat.get_items.to_html %>The examples/ directory contains self-contained scripts demonstrating each feature:
# Run a quick overview (~1 minute)
bundle exec ruby examples/01_quick.rb
# Run all examples
bundle exec ruby examples/all.rb
# Run any individual example
bundle exec ruby examples/02_core.rb| File | Feature |
|---|---|
01_quick.rb |
Quick overview of key features |
02_core.rb |
Basic chat, messages, and responses |
03_multimodal.rb |
Images and basic file handling |
04_file_handling_comprehensive.rb |
PDFs, text files, Rails uploads |
05_structured_output.rb |
Basic structured output |
06_structured_output_comprehensive.rb |
All supported schema formats |
07_edge_cases.rb |
Error handling and edge cases |
08_additional_patterns.rb |
Direct add method, web search + schema |
09_mixed_content.rb |
Combining text and images |
10_image_generation.rb |
Image generation tool |
11_code_interpreter.rb |
Code interpreter tool |
12_background_mode.rb |
Background mode |
13_conversation_features_comprehensive.rb |
Conversation auto-creation and continuity |
14_schema_generation.rb |
Generate schemas from descriptions |
15_proxy.rb |
Proxy support |
16_get_items.rb |
Inspecting conversation items |
17_verbosity.rb |
Verbosity control |
See CONTRIBUTING.md.