AI Chat
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.
Quick Start
Add to your Gemfile and install:
gem "ai-chat", "< 1.0.0"bundle installSet 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.
It's Just an Array of Hashes
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.
# => [
# {
# :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. = @conversation. # Load from your database
chat.user("What should I do next?")
chat.generate!
Adding Messages
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")
Configuration
Model
The gem defaults to gpt-5.2. You can change it:
chat = AI::Chat.new
chat.model = "gpt-4o"
API Key
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-...")
Proxy (Prepend.me)
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 = true
When proxy is enabled, API calls are routed through Prepend.me, and the gem uses AICHAT_PROXY_KEY instead of OPENAI_API_KEY.
Web Search
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!
Including Images
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).
Including 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!
Structured Output
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] # => 285
When 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.
Generating a Schema
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.
Image Generation
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!
Configuring the Tool
To configure the tool, pass a Hash of options instead of true:
chat.image_generation = {
size: "1536x1024",
quality: "low",
model: "gpt-image-2"
}
Supported keys include size, quality, model, action, background, moderation, output_format, output_compression, input_image_mask, and input_fidelity. The Hash is passed through to the OpenAI image generation tool, so refer to those docs for the full list of supported values.
The file extension of saved images is chosen automatically from the decoded bytes, so output_format: "jpeg" writes a .jpg and output_format: "webp" writes a .webp.
partial_imagesis not listed above because this gem uses a blocking call to the Responses API. Partial images only stream whenstream: trueis set, which this gem doesn't yet support.
Code Interpreter
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).
Inspecting Your Conversation
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.
Building Conversations Without API Calls
You 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. = [
{ 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.
Advanced
Reasoning Effort
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.
Verbosity
Control how concise or thorough the model's response is:
chat = AI::Chat.new
chat.verbosity = :low # :low, :medium, or :high
Low verbosity is good for short answers and simple code generation. High verbosity is better for thorough explanations and detailed analysis. Defaults to :medium.
Background Mode
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
= chat.get_response(wait: true, timeout: 600)
puts [:content]
Conversation Management
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!
Response Details
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..."
Inspecting Conversation Items
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
end
HTML Output
All display objects have a to_html method for rendering in ERB templates:
<%= @chat.to_html %>
<%= @chat.get_items.to_html %>
Examples
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 |
Contributing
See CONTRIBUTING.md.