ai-chat 0.5.8 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +303 -671
  3. data/ai-chat.gemspec +2 -2
  4. data/lib/ai/chat.rb +73 -31
  5. metadata +3 -3
data/README.md CHANGED
@@ -1,927 +1,559 @@
1
1
  # AI Chat
2
2
 
3
- This gem provides a class called `AI::Chat` that is intended to make it as easy as possible to use OpenAI's cutting-edge generative AI models.
3
+ 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.
4
4
 
5
- ## Examples
6
-
7
- This gem includes comprehensive example scripts that showcase all features and serve as both documentation and validation tests. To explore the capabilities:
5
+ ## Quick Start
8
6
 
9
- ### Quick Start
10
-
11
- ```bash
12
- # Run a quick overview of key features (takes ~1 minute)
13
- bundle exec ruby examples/01_quick.rb
14
- ```
7
+ 1. Add to your Gemfile and install:
15
8
 
16
- ### Run All Examples
9
+ ```ruby
10
+ gem "ai-chat", "< 1.0.0"
11
+ ```
17
12
 
18
- ```bash
19
- # Run the complete test suite demonstrating all features
20
- bundle exec ruby examples/all.rb
21
- ```
13
+ ```
14
+ bundle install
15
+ ```
22
16
 
23
- ### Individual Feature Examples
24
-
25
- The `examples/` directory contains focused examples for specific features:
26
-
27
- - `01_quick.rb` - Quick overview of key features
28
- - `02_core.rb` - Core functionality (basic chat, messages, responses)
29
- - `03_multimodal.rb` - Basic file and image handling
30
- - `04_file_handling_comprehensive.rb` - Advanced file handling (PDFs, text files, Rails uploads)
31
- - `05_structured_output.rb` - Basic structured output with schemas
32
- - `06_structured_output_comprehensive.rb` - All 6 supported schema formats
33
- - `07_edge_cases.rb` - Error handling and edge cases
34
- - `08_additional_patterns.rb` - Less common usage patterns (direct add method, web search + schema, etc.)
35
- - `09_mixed_content.rb` - Combining text and images in messages
36
- - `10_image_generation.rb` - Using the image generation tool
37
- - `11_code_interpreter.rb` - Using the code interpreter tool
38
- - `12_background_mode.rb` - Running responses in background mode
39
- - `13_conversation_features_comprehensive.rb` - Conversation features (auto-creation, continuity, inspection)
40
- - `14_schema_generation.rb` - Generate JSON schemas from natural language
41
- - `15_proxy.rb` - Proxy support for student accounts
42
- - `16_get_items.rb` - Inspecting conversation items (reasoning, web searches, image generation)
43
-
44
- Each example is self-contained and can be run individually:
45
- ```bash
46
- bundle exec ruby examples/[filename]
47
- ```
17
+ 2. Set up your API key in a `.env` file at the root of your project:
48
18
 
49
- ## Installation
19
+ ```
20
+ AICHAT_PROXY=true
21
+ AICHAT_PROXY_KEY=your-key-from-prepend-me
22
+ ```
50
23
 
51
- ### Gemfile way (preferred)
24
+ (If you have your own OpenAI account, you can skip proxy mode and set `OPENAI_API_KEY` instead.)
52
25
 
53
- Add this line to your application's Gemfile:
26
+ 3. Use it:
54
27
 
55
- ```ruby
56
- gem "ai-chat", "< 1.0.0"
57
- ```
28
+ ```ruby
29
+ require "dotenv/load"
30
+ require "ai-chat"
58
31
 
59
- And then, at a command prompt:
32
+ chat = AI::Chat.new
33
+ chat.user("What is Ruby?")
34
+ response = chat.generate!
60
35
 
61
- ```
62
- bundle install
63
- ```
36
+ ap response
37
+ ```
64
38
 
65
- ### Direct way
39
+ That's it. `generate!` returns the assistant's reply as a `Hash`, and `chat.messages` holds the full conversation as an `Array` of `Hash`es you can inspect, loop through, or store in a database.
66
40
 
67
- Or, install it directly with:
41
+ ## It's Just an Array of Hashes
68
42
 
69
- ```
70
- gem install ai-chat
71
- ```
43
+ Every conversation with an AI model is an array of hashes. Each hash has two keys:
72
44
 
73
- ## Simplest usage
45
+ - `:role` -- who's speaking (`"system"`, `"user"`, or `"assistant"`)
46
+ - `:content` -- what they said
74
47
 
75
- In your Ruby program:
48
+ Here's what a conversation looks like:
76
49
 
77
50
  ```ruby
78
- require "ai-chat"
79
-
80
- # Create an instance of AI::Chat
81
- a = AI::Chat.new
82
-
83
- # Build up your conversation by adding messages
84
- a.add("If the Ruby community had an official motto, what might it be?")
85
-
86
- # See the convo so far - it's just an array of hashes!
87
- a.messages
88
- # => [
89
- # {
90
- # :role => "user",
91
- # :content => "If the Ruby community had an official motto, what might it be?"
92
- # }
93
- # ]
51
+ chat = AI::Chat.new
52
+ chat.user("If Ruby had an official motto, what might it be?")
53
+ response = chat.generate!
94
54
 
95
- # Generate the next message using AI
96
- a.generate!
55
+ ap response
97
56
  # => {
98
57
  # :role => "assistant",
99
- # :content => "Matz is nice and so we are nice",
100
- # :response => { ... }
58
+ # :content => "Matz is nice and so we are nice.",
59
+ # :response => { id: "resp_abc...", model: "gpt-5.2", ... }
101
60
  # }
102
61
 
103
- # Your array now includes the assistant's response
104
- a.messages
62
+ ap chat.messages
105
63
  # => [
106
64
  # {
107
65
  # :role => "user",
108
- # :content => "If the Ruby community had an official motto, what might it be?"
66
+ # :content => "If Ruby had an official motto, what might it be?"
109
67
  # },
110
68
  # {
111
69
  # :role => "assistant",
112
- # :content => "Matz is nice and so we are nice",
70
+ # :content => "Matz is nice and so we are nice.",
113
71
  # :response => { id: "resp_abc...", model: "gpt-5.2", ... }
114
72
  # }
115
73
  # ]
116
-
117
- # Continue the conversation
118
- a.add("What about Rails?")
119
- a.generate!
120
- # => {
121
- # :role => "assistant",
122
- # :content => "Convention over configuration.",
123
- # :response => { ... }
124
- # }
125
74
  ```
126
75
 
127
- ## Understanding the Data Structure
76
+ `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`.
128
77
 
129
- Every OpenAI chat is just an array of hashes. Each hash needs:
130
- - `:role`: who's speaking ("system", "user", or "assistant")
131
- - `:content`: what they're saying
78
+ This design is intentional:
132
79
 
133
- That's it! You're building something like this:
80
+ - **You can see what you're building.** `ap chat.messages` at any point shows the exact data structure.
81
+ - **It reinforces Ruby fundamentals.** Arrays, hashes, symbols -- you already know these.
82
+ - **It's flexible.** The same structure works when loading messages from a database:
134
83
 
135
84
  ```ruby
136
- [
137
- {
138
- :role => "system",
139
- :content => "You are a helpful assistant"
140
- },
141
- {
142
- :role => "user",
143
- :content => "Hello!"
144
- },
145
- {
146
- :role => "assistant",
147
- :content => "Hi there! How can I help you today?",
148
- :response => { id: "resp_abc...", model: "gpt-5.2", ... }
149
- }
150
- ]
85
+ chat = AI::Chat.new
86
+ chat.messages = @conversation.messages # Load from your database
87
+ chat.user("What should I do next?")
88
+ chat.generate!
151
89
  ```
152
90
 
153
- That last bit, under `:response`, is an object that represents the JSON that the OpenAI API sent back to us. It contains information about the number of tokens consumed, as well as a response ID that we can use later if we want to pick up the conversation at that point. More on that later.
91
+ ## Adding Messages
154
92
 
155
- ## Adding Different Types of Messages
93
+ The `user` method adds a message with `role: "user"` and `generate!` sends the conversation to the API and returns the assistant's reply:
156
94
 
157
95
  ```ruby
158
- require "ai-chat"
159
-
160
- b = AI::Chat.new
161
-
162
- # Add system instructions
163
- b.add("You are a helpful assistant that talks like Shakespeare.", role: "system")
96
+ chat = AI::Chat.new
97
+ chat.user("Hello!")
98
+ ap chat.generate!
164
99
 
165
- # Add a user message (role defaults to "user")
166
- b.add("If the Ruby community had an official motto, what might it be?")
100
+ # Continue the conversation
101
+ chat.user("What about Rails?")
102
+ ap chat.generate!
103
+ ```
167
104
 
168
- # Check what we've built
169
- b.messages
170
- # => [
171
- # {
172
- # :role => "system",
173
- # :content => "You are a helpful assistant that talks like Shakespeare."
174
- # },
175
- # {
176
- # :role => "user",
177
- # :content => "If the Ruby community had an official motto, what might it be?"
178
- # }
179
- # ]
105
+ You can also add system instructions (to guide the model's behavior) and manually add assistant messages (to reconstruct past conversations):
180
106
 
181
- # Generate a response
182
- b.generate!
183
- # => {
184
- # :role => "assistant",
185
- # :content => "Methinks 'tis 'Ruby doth bring joy to all who craft with care'",
186
- # :response => { ... }
187
- # }
107
+ ```ruby
108
+ chat = AI::Chat.new
109
+ chat.system("You are a helpful assistant that talks like Shakespeare.")
110
+ chat.user("What is Ruby?")
111
+ chat.generate!
188
112
  ```
189
113
 
190
- ### Convenience Methods
191
-
192
- Instead of always specifying the role, you can use these shortcuts:
114
+ Under the hood, these are shortcuts for the `add` method:
193
115
 
194
116
  ```ruby
195
- c = AI::Chat.new
196
-
197
117
  # These are equivalent:
198
- c.add("You are helpful", role: "system")
199
- c.system("You are helpful")
118
+ chat.system("You are helpful")
119
+ chat.add("You are helpful", role: "system")
200
120
 
201
121
  # These are equivalent:
202
- c.add("Hello there!")
203
- c.user("Hello there!")
122
+ chat.user("Hello!")
123
+ chat.add("Hello!") # role defaults to "user"
204
124
 
205
125
  # These are equivalent:
206
- c.add("Hi! How can I help?", role: "assistant")
207
- c.assistant("Hi! How can I help?")
208
- ```
209
-
210
- ## Why This Design?
211
-
212
- We use the `add` method (and its shortcuts) to build up an array because:
213
-
214
- 1. **It's educational**: You can see exactly what data structure you're building
215
- 2. **It's debuggable**: Use `pp a.messages` anytime to inspect your conversation
216
- 3. **It's flexible**: The same pattern works when loading existing conversations:
217
-
218
- ```ruby
219
- # In a Rails app, you might do:
220
- d = AI::Chat.new
221
- d.messages = @conversation.messages # Load existing messages
222
- d.user("What should I do next?") # Add a new question
223
- d.generate! # Generate a response
126
+ chat.assistant("Here's what I think...")
127
+ chat.add("Here's what I think...", role: "assistant")
224
128
  ```
225
129
 
226
130
  ## Configuration
227
131
 
228
132
  ### Model
229
133
 
230
- By default, the gem uses OpenAI's `gpt-5.2` model. If you want to use a different model, you can set it:
134
+ The gem defaults to `gpt-5.2`. You can change it:
231
135
 
232
136
  ```ruby
233
- e = AI::Chat.new
234
- e.model = "gpt-4o"
137
+ chat = AI::Chat.new
138
+ chat.model = "gpt-4o"
235
139
  ```
236
140
 
237
- See [OpenAI's model documentation](https://platform.openai.com/docs/models) for available models.
141
+ ### API Key
238
142
 
239
- ### API key
143
+ By default, the gem looks for an environment variable based on whether proxy mode is on or off:
240
144
 
241
- The gem by default looks for `AICHAT_API_KEY` first. If that is missing (or empty), it falls back to `OPENAI_API_KEY`.
145
+ | Mode | Environment variable |
146
+ |---|---|
147
+ | Proxy on (`AICHAT_PROXY=true`) | `AICHAT_PROXY_KEY` |
148
+ | Proxy off (default) | `OPENAI_API_KEY` |
242
149
 
243
- You can specify a different environment variable name:
150
+ You can also specify a custom environment variable name or pass the key directly:
244
151
 
245
152
  ```ruby
246
- f = AI::Chat.new(api_key_env_var: "MY_OPENAI_TOKEN")
247
- ```
153
+ # Use a different environment variable
154
+ chat = AI::Chat.new(api_key_env_var: "MY_OPENAI_TOKEN")
248
155
 
249
- Or, you can pass an API key in directly:
250
-
251
- ```ruby
252
- g = AI::Chat.new(api_key: "your-api-key-goes-here")
156
+ # Or pass the key directly
157
+ chat = AI::Chat.new(api_key: "sk-...")
253
158
  ```
254
159
 
255
- ## Inspecting Your Conversation
160
+ ## Proxy (Prepend.me)
256
161
 
257
- You can call `.messages` to get an array containing the conversation so far:
162
+ If you're using a [Prepend.me](https://prepend.me/) proxy key (common in classroom settings), add these to your `.env` file:
258
163
 
259
- ```ruby
260
- h = AI::Chat.new
261
- h.system("You are a helpful cooking assistant")
262
- h.user("How do I boil an egg?")
263
- h.generate!
164
+ ```
165
+ AICHAT_PROXY=true
166
+ AICHAT_PROXY_KEY=your-key-from-prepend-me
167
+ ```
264
168
 
265
- # See the whole conversation
266
- h.messages
267
- # => [
268
- # {
269
- # :role => "system",
270
- # :content => "You are a helpful cooking assistant"
271
- # },
272
- # {
273
- # :role => "user",
274
- # :content => "How do I boil an egg?"
275
- # },
276
- # {
277
- # :role => "assistant",
278
- # :content => "Here's how to boil an egg..."
279
- # }
280
- # ]
169
+ You can also enable proxy mode in code:
281
170
 
282
- # Get just the last response
283
- h.messages.last[:content]
284
- # => "Here's how to boil an egg..."
171
+ ```ruby
172
+ # At construction time
173
+ chat = AI::Chat.new(proxy: true)
285
174
 
286
- # Or use the convenient shortcut
287
- h.last[:content]
288
- # => "Here's how to boil an egg..."
175
+ # Or toggle it on an existing instance
176
+ chat = AI::Chat.new
177
+ chat.proxy = true
289
178
  ```
290
179
 
180
+ When proxy is enabled, API calls are routed through Prepend.me, and the gem uses `AICHAT_PROXY_KEY` instead of `OPENAI_API_KEY`.
181
+
291
182
  ## Web Search
292
183
 
293
- To give the model access to real-time information from the internet, you can enable web searching. This uses OpenAI's built-in `web_search` tool.
184
+ Give the model access to current information from the internet:
294
185
 
295
186
  ```ruby
296
- m = AI::Chat.new
297
- m.web_search = true
298
- m.user("What are the latest developments in the Ruby language?")
299
- m.generate! # This may use web search to find current information
187
+ chat = AI::Chat.new
188
+ chat.web_search = true
189
+ chat.user("What are the latest developments in the Ruby language?")
190
+ chat.generate!
300
191
  ```
301
192
 
302
- ## Structured Output
193
+ ## Including Images
303
194
 
304
- Get back Structured Output by setting the `schema` attribute (I suggest using [OpenAI's handy tool for generating the JSON Schema](https://platform.openai.com/docs/guides/structured-outputs)):
195
+ Use the `image:` or `images:` parameter to send images along with your message:
305
196
 
306
197
  ```ruby
307
- i = AI::Chat.new
198
+ chat = AI::Chat.new
308
199
 
309
- i.system("You are an expert nutritionist. The user will describe a meal. Estimate the calories, carbs, fat, and protein.")
200
+ # Single image
201
+ chat.user("What's in this image?", image: "photo.jpg")
202
+ chat.generate!
310
203
 
311
- # The schema should be a JSON string (use OpenAI's tool to generate: https://platform.openai.com/docs/guides/structured-outputs)
312
- i.schema = '{"name": "nutrition_values","strict": true,"schema": {"type": "object","properties": {"fat": {"type": "number","description": "The amount of fat in grams."},"protein": {"type": "number","description": "The amount of protein in grams."},"carbs": {"type": "number","description": "The amount of carbohydrates in grams."},"total_calories": {"type": "number","description": "The total calories calculated based on fat, protein, and carbohydrates."}},"required": ["fat","protein","carbs","total_calories"],"additionalProperties": false}}'
204
+ # Multiple images
205
+ chat.user("Compare these", images: ["image1.jpg", "image2.jpg"])
206
+ chat.generate!
207
+ ```
313
208
 
314
- i.user("1 slice of pizza")
209
+ You can pass local file paths, URLs (`https://...`), or file-like objects (such as `File.open(...)` or Rails uploaded files).
315
210
 
316
- response = i.generate!
317
- data = response[:content]
318
- # => {:fat=>15, :protein=>12, :carbs=>35, :total_calories=>285}
211
+ ## Including Files
319
212
 
320
- # The response is parsed JSON, not a string!
321
- data[:total_calories] # => 285
322
- ```
213
+ Use the `file:` or `files:` parameter to send documents:
323
214
 
324
- ### Schema Formats
215
+ ```ruby
216
+ chat = AI::Chat.new
325
217
 
326
- The gem supports multiple schema formats to accommodate different preferences and use cases. The gem will automatically wrap your schema in the correct format for the API.
218
+ # Single file
219
+ chat.user("Summarize this document", file: "report.pdf")
220
+ chat.generate!
327
221
 
328
- #### 1. Full Schema with `format` Key (Most Explicit)
329
- ```ruby
330
- # When you need complete control over the schema structure
331
- i.schema = {
332
- format: {
333
- type: :json_schema,
334
- name: "nutrition_values",
335
- strict: true,
336
- schema: {
337
- type: "object",
338
- properties: {
339
- fat: { type: "number", description: "The amount of fat in grams." },
340
- protein: { type: "number", description: "The amount of protein in grams." }
341
- },
342
- required: ["fat", "protein"],
343
- additionalProperties: false
344
- }
345
- }
346
- }
222
+ # Multiple files
223
+ chat.user("Compare these", files: ["doc1.pdf", "doc2.txt"])
224
+ chat.generate!
347
225
  ```
348
226
 
349
- #### 2. Schema with `name`, `strict`, and `schema` Keys
227
+ PDFs are sent as attachments. Text-based files have their content extracted and sent as text.
228
+
229
+ You can combine images and files in one message:
230
+
350
231
  ```ruby
351
- # The format shown in OpenAI's documentation
352
- i.schema = {
353
- name: "nutrition_values",
354
- strict: true,
355
- schema: {
356
- type: "object",
357
- properties: {
358
- fat: { type: "number", description: "The amount of fat in grams." },
359
- protein: { type: "number", description: "The amount of protein in grams." }
360
- },
361
- required: [:fat, :protein],
362
- additionalProperties: false
363
- }
364
- }
232
+ chat.user("Analyze these materials",
233
+ images: ["chart1.png", "chart2.png"],
234
+ files: ["report.pdf", "data.csv"])
235
+ chat.generate!
365
236
  ```
366
237
 
367
- #### 3. Simple JSON Schema Object
238
+ ## Structured Output
239
+
240
+ 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:
241
+
368
242
  ```ruby
369
- # The simplest format - just provide the schema itself
370
- # The gem will wrap it with sensible defaults (name: "response", strict: true)
371
- i.schema = {
243
+ chat = AI::Chat.new
244
+ chat.system("You are an expert nutritionist. Estimate the nutritional content of the meal the user describes.")
245
+
246
+ chat.schema = {
372
247
  type: "object",
373
248
  properties: {
374
- fat: { type: "number", description: "The amount of fat in grams." },
375
- protein: { type: "number", description: "The amount of protein in grams." }
249
+ fat: { type: "number", description: "Fat in grams" },
250
+ protein: { type: "number", description: "Protein in grams" },
251
+ carbs: { type: "number", description: "Carbohydrates in grams" },
252
+ calories: { type: "number", description: "Total calories" }
376
253
  },
377
- required: ["fat", "protein"],
254
+ required: ["fat", "protein", "carbs", "calories"],
378
255
  additionalProperties: false
379
256
  }
380
- ```
381
-
382
- #### 4. JSON String Formats
383
- All the above formats also work as JSON strings:
384
-
385
- ```ruby
386
- # As a JSON string with full format
387
- i.schema = '{"format":{"type":"json_schema","name":"nutrition_values","strict":true,"schema":{...}}}'
388
257
 
389
- # As a JSON string with name/strict/schema
390
- i.schema = '{"name":"nutrition_values","strict":true,"schema":{...}}'
391
-
392
- # As a simple JSON schema string
393
- i.schema = '{"type":"object","properties":{...}}'
394
- ```
395
-
396
- ### Generating a Schema
258
+ chat.user("1 slice of pizza")
259
+ response = chat.generate!
397
260
 
398
- You can call the class method `AI::Chat.generate_schema!` to use OpenAI to generate a JSON schema for you given a `String` describing the schema you want.
261
+ data = response[:content]
262
+ # => { fat: 15, protein: 12, carbs: 35, calories: 285 }
399
263
 
400
- ```rb
401
- AI::Chat.generate_schema!("A user profile with name (required), email (required), age (number), and bio (optional text).")
402
- # => "{ ... }"
264
+ data[:calories] # => 285
403
265
  ```
404
266
 
405
- This method returns a String containing the JSON schema. The JSON schema also writes (or overwrites) to `schema.json` at the root of the project.
406
-
407
- Similar to generating messages with `AI::Chat` objects, this class method will look for `AICHAT_API_KEY` first, then fall back to `OPENAI_API_KEY` if needed. You can also pass the API key directly or choose a different environment variable key for it to use.
267
+ When a schema is set, `generate!` returns a parsed Ruby `Hash` with symbolized keys instead of a `String`.
408
268
 
409
- ```rb
410
- # Passing the API key directly
411
- AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", api_key: "MY_SECRET_API_KEY")
269
+ 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.
412
270
 
413
- # Choosing a different API key name
414
- AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", api_key_env_var: "CUSTOM_KEY")
415
- ```
271
+ ### Generating a Schema
416
272
 
417
- `generate_schema!` also follows proxy defaults from the `AICHAT_PROXY` environment variable. Proxy is enabled only when `AICHAT_PROXY` is exactly `"true"`.
273
+ You can use AI to generate a schema from a plain English description:
418
274
 
419
- ```bash
420
- export AICHAT_PROXY=true
275
+ ```ruby
276
+ AI::Chat.generate_schema!("A user profile with name (required), email (required), age (number), and bio (optional).")
421
277
  ```
422
278
 
423
- If you pass `proxy: true` or `proxy: false`, that explicit value overrides the env default.
279
+ 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.
424
280
 
425
- You can choose a location for the schema to save by using the `location` keyword argument.
426
-
427
- ```rb
428
- AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", location: "my_schemas/user.json")
429
- ```
281
+ ## Image Generation
430
282
 
431
- If you don't want to write the output to a file, you can pass `false` to `location`.
283
+ Enable OpenAI's image generation tool to create images from descriptions:
432
284
 
433
- ```rb
434
- AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", location: false)
435
- # => { ... }
285
+ ```ruby
286
+ chat = AI::Chat.new
287
+ chat.image_generation = true
288
+ chat.user("Draw a picture of a kitten")
289
+ chat.generate!
436
290
  ```
437
291
 
438
- ### Schema Notes
439
-
440
- - The keys can be `String`s or `Symbol`s.
441
- - The gem automatically converts your schema to the format expected by the API.
442
- - When a schema is set, `generate!` returns a parsed Ruby Hash with symbolized keys, not a String.
443
-
444
- ## Including Images
445
-
446
- You can include images in your chat messages using the `user` method with the `image` or `images` parameter:
292
+ Generated images are saved to `./images` by default (in timestamped subfolders like `./images/20250804T113039_resp_abc123/001.png`). You can change the folder:
447
293
 
448
294
  ```ruby
449
- j = AI::Chat.new
450
-
451
- # Send a single image
452
- j.user("What's in this image?", image: "path/to/local/image.jpg")
453
- j.generate! # => "I can see a sunset over the ocean..."
454
-
455
- # Send multiple images
456
- j.user("Compare these images", images: ["image1.jpg", "image2.jpg"])
457
- j.generate! # => "The first image shows... while the second..."
458
-
459
- # Mix URLs and local files
460
- j.user("What's the difference?", images: [
461
- "local_photo.jpg",
462
- "https://example.com/remote_photo.jpg"
463
- ])
464
- j.generate!
295
+ chat.image_folder = "./my_images"
465
296
  ```
466
297
 
467
- The gem supports three types of image inputs:
468
-
469
- - **URLs**: Pass an image URL starting with `http://` or `https://`
470
- - **File paths**: Pass a string with a path to a local image file
471
- - **File-like objects**: Pass an object that responds to `read` (like `File.open("image.jpg")` or Rails uploaded files)
472
-
473
- ## Including Files
474
-
475
- You can include files (PDFs, text files, etc.) in your messages using the `file` or `files` parameter:
298
+ The assistant's message will include an `:images` key with the saved file paths:
476
299
 
477
300
  ```ruby
478
- k = AI::Chat.new
479
-
480
- # Send a single file
481
- k.user("Summarize this document", file: "report.pdf")
482
- k.generate!
483
-
484
- # Send multiple files
485
- k.user("Compare these documents", files: ["doc1.pdf", "doc2.txt", "data.json"])
486
- k.generate!
301
+ chat.last[:images]
302
+ # => ["./images/20250804T113039_resp_abc123/001.png"]
487
303
  ```
488
304
 
489
- Files are handled intelligently based on their type:
490
- - **PDFs**: Sent as file attachments for the model to analyze
491
- - **Text files**: Content is automatically extracted and sent as text
492
- - **Other formats**: The gem attempts to read them as text if possible
493
-
494
- ## Mixed Content (Images + Files)
495
-
496
- You can send images and files together in a single message:
305
+ AI-generated images are stored by OpenAI, so you can refine them in follow-up messages without re-sending:
497
306
 
498
307
  ```ruby
499
- l = AI::Chat.new
500
-
501
- # Mix image and file in one message
502
- l.user("Compare this photo with the document",
503
- image: "photo.jpg",
504
- file: "document.pdf")
505
- l.generate!
506
-
507
- # Mix multiple images and files
508
- l.user("Analyze all these materials",
509
- images: ["chart1.png", "chart2.png"],
510
- files: ["report.pdf", "data.csv"])
511
- l.generate!
308
+ chat.user("Make it even cuter")
309
+ chat.generate!
512
310
  ```
513
311
 
514
- **Note**: Images should use `image:`/`images:` parameters, while documents should use `file:`/`files:` parameters.
515
-
516
- ## Image generation
312
+ ### Configuring the Tool
517
313
 
518
- You can enable OpenAI's image generation tool:
314
+ To configure the tool, pass a `Hash` of options instead of `true`:
519
315
 
520
316
  ```ruby
521
- a = AI::Chat.new
522
- a.image_generation = true
523
- a.user("Draw a picture of a kitten")
524
- a.generate!
525
- # => {
526
- # :content => "Here is your picture of a kitten:",
527
- # :response => { ... }
528
- # }
317
+ chat.image_generation = {
318
+ size: "1536x1024",
319
+ quality: "low",
320
+ model: "gpt-image-2"
321
+ }
529
322
  ```
530
323
 
531
- By default, images are saved to `./images`. You can configure a different location:
324
+ 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](https://platform.openai.com/docs/guides/image-generation), so refer to those docs for the full list of supported values.
532
325
 
533
- ```ruby
534
- a = AI::Chat.new
535
- a.image_generation = true
536
- a.image_folder = "./my_images"
537
- a.user("Draw a picture of a kitten")
538
- a.generate!
539
- # => {
540
- # :content => "Here is your picture of a kitten:",
541
- # :response => { ... }
542
- # }
543
- ```
326
+ 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`.
544
327
 
545
- Images are saved in timestamped subfolders using ISO 8601 basic format. For example:
546
- - `./images/20250804T11303912_resp_abc123/001.png`
547
- - `./images/20250804T11303912_resp_abc123/002.png` (if multiple images)
328
+ > `partial_images` is not listed above because this gem uses a blocking call to the Responses API. Partial images only stream when `stream: true` is set, which this gem doesn't yet support.
548
329
 
549
- The folder structure ensures images are organized chronologically and by response.
330
+ ## Code Interpreter
550
331
 
551
- The messages array will now look like this:
332
+ 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:
552
333
 
553
334
  ```ruby
554
- a.messages
555
- # => [
556
- # {
557
- # :role => "user",
558
- # :content => "Draw a picture of a kitten"
559
- # },
560
- # {
561
- # :role => "assistant",
562
- # :content => "Here is your picture of a kitten:",
563
- # :images => [ "./images/20250804T11303912_resp_abc123/001.png" ],
564
- # :response => { ... }
565
- # }
566
- # ]
335
+ chat = AI::Chat.new
336
+ chat.code_interpreter = true
337
+ chat.user("Plot y = 2x^3 for x from -5 to 5")
338
+ chat.generate!
567
339
  ```
568
340
 
569
- You can access the image filenames in several ways:
341
+ The model will write a Python script, execute it, and return the result (including any generated files like charts).
570
342
 
571
- ```ruby
572
- # From the last message
573
- images = a.messages.last[:images]
574
- # => ["./images/20250804T11303912_resp_abc123/001.png"]
575
-
576
- # From the response object
577
- images = a.messages.last.dig(:response, :images)
578
- # => ["./images/20250804T11303912_resp_abc123/001.png"]
579
- ```
343
+ ## Inspecting Your Conversation
580
344
 
581
- Note: Unlike with user-provided input images, OpenAI _does_ store AI-generated output images. So, if you make another API request using the same chat, previous images generated by the model in the conversation history will automatically be used — you don't have to re-send them. This allows you to easily refine an image with user input over multi-turn chats.
345
+ You can look at the conversation at any point:
582
346
 
583
347
  ```ruby
584
- a = AI::Chat.new
585
- a.image_generation = true
586
- a.image_folder = "./images"
587
- a.user("Draw a picture of a kitten")
588
- a.generate!
589
- # => { :content => "Here is a picture of a kitten:", ... }
590
- a.user("Make it even cuter")
591
- a.generate!
592
- # => { :content => "Here is the kitten, but even cuter:", ... }
593
- ```
348
+ chat = AI::Chat.new
349
+ chat.system("You are a helpful cooking assistant")
350
+ chat.user("How do I boil an egg?")
351
+ response = chat.generate!
594
352
 
595
- ## Code Interpreter
353
+ # The return value is the assistant's reply
354
+ response[:content]
355
+ # => "Here's how to boil an egg..."
596
356
 
597
- ```ruby
598
- y = AI::Chat.new
599
- y.code_interpreter = true
600
- y.user("Plot y = 2x*3 when x is -5 to 5.")
601
- y.generate!
602
- # => { :content => "Here is the graph.", ... }
357
+ # See the whole conversation
358
+ ap chat.messages
603
359
  ```
604
360
 
605
- ## Background mode
361
+ ## Building Conversations Without API Calls
606
362
 
607
- If you want to start a response and poll for it later, set `background = true` before calling `generate!`:
363
+ You can manually build up a conversation without calling the API, which is useful for reconstructing a past conversation from your database:
608
364
 
609
365
  ```ruby
610
366
  chat = AI::Chat.new
611
- chat.background = true
612
- chat.user("Write a short description about a sci-fi novel about a rat in space.")
613
- chat.generate!
614
-
615
- # Poll until it completes (this updates the existing assistant message)
616
- message = chat.get_response(wait: true, timeout: 600)
617
- puts message[:content]
618
- ```
367
+ chat.system("You are a helpful assistant who provides information about planets.")
619
368
 
620
- ## Proxying Through prepend.me
369
+ chat.user("Tell me about Mars.")
370
+ chat.assistant("Mars is the fourth planet from the Sun....")
621
371
 
622
- You can proxy API calls through [prepend.me](https://prepend.me/).
372
+ chat.user("What's the atmosphere like?")
373
+ chat.assistant("Mars has a very thin atmosphere compared to Earth....")
623
374
 
624
- ```rb
625
- chat = AI::Chat.new
626
- chat.proxy = true
627
- chat.user("Tell me a story")
375
+ # Now continue with an API-generated response
376
+ chat.user("Are there any current missions?")
628
377
  chat.generate!
629
- puts chat.last[:content]
630
- # => "Once upon a time..."
631
378
  ```
632
379
 
633
- You can also default proxy mode from the environment for both `AI::Chat.new` and `AI::Chat.generate_schema!`:
380
+ You can also set all messages at once with an array of hashes:
634
381
 
635
- ```bash
636
- export AICHAT_PROXY=true
382
+ ```ruby
383
+ chat = AI::Chat.new
384
+ chat.messages = [
385
+ { role: "system", content: "You are a helpful assistant." },
386
+ { role: "user", content: "Tell me about Mars." },
387
+ { role: "assistant", content: "Mars is the fourth planet from the Sun...." },
388
+ { role: "user", content: "What's the atmosphere like?" },
389
+ { role: "assistant", content: "Mars has a very thin atmosphere...." }
390
+ ]
391
+
392
+ chat.user("Could it support human life?")
393
+ chat.generate!
637
394
  ```
638
395
 
639
- Proxy is enabled only when `AICHAT_PROXY` is exactly `"true"`. Any other value (including `"TRUE"` or `"1"`) leaves proxy disabled unless you explicitly set `chat.proxy = true` or pass `proxy: true`.
396
+ For messages with images or files, use `chat.user(..., image:, file:)` instead so the gem can build the correct multimodal structure.
640
397
 
641
- When proxy is enabled, **you must use the API key provided by prepend.me** in place of a real OpenAI API key. Refer to [the section on API keys](#api-key) for options on how to set your key.
398
+ ## Advanced
642
399
 
643
- ## Building Conversations Without API Calls
400
+ ### Reasoning Effort
644
401
 
645
- You can manually add assistant messages without making API calls, which is useful when reconstructing a past conversation:
402
+ Control how much reasoning the model does before responding:
646
403
 
647
404
  ```ruby
648
- # Create a new chat instance
649
- k = AI::Chat.new
650
-
651
- # Add previous messages
652
- k.system("You are a helpful assistant who provides information about planets.")
653
-
654
- k.user("Tell me about Mars.")
655
- k.assistant("Mars is the fourth planet from the Sun....")
656
-
657
- k.user("What's the atmosphere like?")
658
- k.assistant("Mars has a very thin atmosphere compared to Earth....")
659
-
660
- k.user("Could it support human life?")
661
- k.assistant("Mars currently can't support human life without....")
405
+ chat = AI::Chat.new
406
+ chat.reasoning_effort = "high" # "low", "medium", or "high"
662
407
 
663
- # Now continue the conversation with an API-generated response
664
- k.user("Are there any current missions to go there?")
665
- response = k.generate!
666
- puts response
408
+ chat.user("Explain the tradeoffs between microservices and monoliths.")
409
+ chat.generate!
667
410
  ```
668
411
 
669
- With this, you can loop through any conversation's history (perhaps after retrieving it from your database), recreate an `AI::Chat`, and then continue it.
412
+ By default, `reasoning_effort` is `nil` (no reasoning parameter is sent). For `gpt-5.2`, this is equivalent to no reasoning.
670
413
 
671
- ## Reasoning Effort
414
+ ### Verbosity
672
415
 
673
- You can control how much reasoning the model does before producing its response:
416
+ Control how concise or thorough the model's response is:
674
417
 
675
418
  ```ruby
676
- l = AI::Chat.new
677
- l.reasoning_effort = "low" # Can be "low", "medium", or "high"
678
-
679
- l.user("What does this error message mean? <insert error message>")
680
- l.generate!
419
+ chat = AI::Chat.new
420
+ chat.verbosity = :low # :low, :medium, or :high
681
421
  ```
682
422
 
683
- The `reasoning_effort` parameter guides the model on how many reasoning tokens to generate. Options are:
684
- - `"low"`: Favors speed and economical token usage.
685
- - `"medium"`: Balances speed and reasoning accuracy.
686
- - `"high"`: Favors more complete reasoning.
687
-
688
- By default, `reasoning_effort` is `nil`, which means no reasoning parameter is sent to the API. For `gpt-5.2` (the default model), this is equivalent to `"none"` reasoning.
689
-
690
- ## Verbosity
691
-
692
- Verbosity determines how many output tokens are generated. Lowering the number of tokens reduces overall latency. While the model's reasoning approach stays mostly the same, the model finds ways to answer more concisely—which can either improve or diminish answer quality, depending on your use case. Here are some scenarios for both ends of the verbosity spectrum:
423
+ Low verbosity is good for short answers and simple code generation. High verbosity is better for thorough explanations and detailed analysis. Defaults to `:medium`.
693
424
 
694
- - High verbosity: Use when you need the model to provide thorough explanations of documents or perform extensive code refactoring.
695
- - Low verbosity: Best for situations where you want concise answers or simple code generation, such as SQL queries.
425
+ ### Background Mode
696
426
 
697
- The supported values are `:high`, `:medium`, or `:low`. The default value is `:medium` for `gpt-5.2`. **Older models (like `gpt-4.1-nano`) only support `:medium`**.
698
-
699
- ## Advanced: Response Details
700
-
701
- When you call `generate!` (or later call `get_response` in background mode), the gem stores additional information about the API response:
427
+ Start a response and poll for it later:
702
428
 
703
429
  ```ruby
704
- t = AI::Chat.new
705
- t.user("Hello!")
706
- t.generate!
707
-
708
- # Each assistant message includes a response object
709
- t.messages.last
710
- # => {
711
- # :role => "assistant",
712
- # :content => "Hello! How can I help you today?",
713
- # :response => { id: "resp_abc...", model: "gpt-5.2", ... }
714
- # }
430
+ chat = AI::Chat.new
431
+ chat.background = true
432
+ chat.user("Write a detailed analysis of Ruby's GC implementation.")
433
+ chat.generate!
715
434
 
716
- # Access detailed information
717
- response = t.last[:response]
718
- response[:id] # => "resp_abc123..."
719
- response[:model] # => "gpt-5.2"
720
- response[:usage] # => {:input_tokens=>5, :output_tokens=>7, :total_tokens=>12}
435
+ # Poll until it completes
436
+ message = chat.get_response(wait: true, timeout: 600)
437
+ puts message[:content]
721
438
  ```
722
439
 
723
- This information is useful for:
440
+ ### Conversation Management
724
441
 
725
- - Debugging and monitoring token usage.
726
- - Understanding which model was actually used.
727
- - Future features like cost tracking.
728
-
729
- ### Last Response ID
730
-
731
- In addition to the `response` object inside each message, the `AI::Chat` instance also provides a convenient reader, `last_response_id`, which always holds the ID of the most recent response.
442
+ The gem automatically creates a server-side conversation on your first `generate!` call:
732
443
 
733
444
  ```ruby
734
445
  chat = AI::Chat.new
735
446
  chat.user("Hello")
736
447
  chat.generate!
737
448
 
738
- puts chat.last_response_id # => "resp_abc123..."
449
+ chat.conversation_id # => "conv_abc123..."
739
450
 
740
- chat.user("Goodbye")
451
+ # The model remembers context across messages
452
+ chat.user("What did I just say?")
741
453
  chat.generate!
742
-
743
- puts chat.last_response_id # => "resp_xyz789..." (a new ID)
744
454
  ```
745
455
 
746
- This is particularly useful for background mode workflows. If you want to retrieve or cancel a background response from a different process, use `OpenAI::Client` directly:
456
+ You can load an existing conversation:
747
457
 
748
458
  ```ruby
749
- require "openai"
750
-
751
- api_key = ENV["AICHAT_API_KEY"]
752
- api_key = ENV.fetch("OPENAI_API_KEY") if api_key.nil? || api_key.empty?
753
- client = OpenAI::Client.new(api_key: api_key)
754
-
755
- response_id = "resp_abc123..." # e.g., load from your database
756
- response = client.responses.retrieve(response_id)
459
+ chat = AI::Chat.new
460
+ chat.conversation_id = @thread.conversation_id # From your database
757
461
 
758
- client.responses.cancel(response_id) unless response.status.to_s == "completed"
462
+ chat.user("Continue our discussion")
463
+ chat.generate!
759
464
  ```
760
465
 
761
- ### Automatic Conversation Management
466
+ ### Response Details
762
467
 
763
- Starting with your first `generate!` call, the gem automatically creates and manages a conversation with OpenAI. This conversation is stored server-side and tracks all messages, tool calls, reasoning, and other items.
468
+ Each assistant message includes an API response hash with metadata:
764
469
 
765
470
  ```ruby
766
471
  chat = AI::Chat.new
767
- chat.user("Hello")
472
+ chat.user("Hello!")
768
473
  chat.generate!
769
474
 
770
- # Conversation ID is automatically set
771
- puts chat.conversation_id # => "conv_abc123..."
772
-
773
- # Continue the conversation - context is automatically maintained
774
- chat.user("What did I just say?")
775
- chat.generate! # Uses the same conversation automatically
475
+ response = chat.last[:response]
476
+ response[:id] # => "resp_abc123..."
477
+ response[:model] # => "gpt-5.2"
478
+ response[:usage] # => { input_tokens: 5, output_tokens: 7, total_tokens: 12 }
776
479
  ```
777
480
 
778
- You can also load an existing conversation from your database:
481
+ The `last_response_id` reader always holds the most recent response ID:
779
482
 
780
483
  ```ruby
781
- # Load stored conversation_id from your database
782
- chat = AI::Chat.new
783
- chat.conversation_id = @thread.conversation_id # From your database
784
-
785
- chat.user("Continue our discussion")
786
- chat.generate! # Uses the loaded conversation
484
+ chat.last_response_id # => "resp_abc123..."
787
485
  ```
788
486
 
789
- ## Inspecting Conversation Details
487
+ ### Inspecting Conversation Items
790
488
 
791
- The `get_items` method fetches all conversation items (messages, tool calls, reasoning, etc.) from the API for both programmatic use and debugging:
489
+ The `get_items` method fetches all conversation items from the API, including messages, tool calls, reasoning steps, and web searches:
792
490
 
793
491
  ```ruby
794
492
  chat = AI::Chat.new
795
- chat.reasoning_effort = "high" # Enable reasoning summaries
493
+ chat.reasoning_effort = "high"
796
494
  chat.web_search = true
797
495
  chat.user("Search for Ruby tutorials")
798
496
  chat.generate!
799
497
 
800
- # Get all conversation items (chronological order by default)
498
+ # Pretty-printed in IRB/console
801
499
  chat.get_items
802
500
 
803
- # Output in IRB/Rails console:
804
- # ┌────────────────────────────────────────────────────────────────────────────┐
805
- # │ Conversation: conv_6903c1eea6cc819695af3a1b1ebf9b390c3db5e8ec021c9a │
806
- # │ Items: 8 │
807
- # └────────────────────────────────────────────────────────────────────────────┘
808
- #
809
- # [detailed colorized output of all items including web searches,
810
- # reasoning summaries, tool calls, messages, etc.]
811
-
812
- # Iterate over items programmatically
501
+ # Iterate programmatically
813
502
  chat.get_items.data.each do |item|
814
503
  case item.type
815
504
  when :message
816
505
  puts "#{item.role}: #{item.content.first.text}"
817
506
  when :web_search_call
818
- puts "Web search: #{item.action.query}" if item.action.respond_to?(:query) && item.action.query
507
+ puts "Searched: #{item.action.query}" if item.action.respond_to?(:query)
819
508
  when :reasoning
820
- # Reasoning summaries show a high-level view of the model's reasoning
821
- if item.summary&.first
822
- puts "Reasoning: #{item.summary.first.text}"
823
- end
824
- when :image_generation_call
825
- puts "Image generated" if item.result
509
+ puts "Reasoning: #{item.summary.first.text}" if item.summary&.first
826
510
  end
827
511
  end
828
-
829
- # For long conversations, you can request reverse chronological order
830
- # (useful for pagination to get most recent items first)
831
- recent_items = chat.get_items(order: :desc)
832
512
  ```
833
513
 
834
- When `reasoning_effort` is set, the API returns reasoning summaries (e.g., "Planning Ruby version search", "Confirming image tool usage"). Note that not all reasoning items have summaries - some intermediate steps may be empty.
835
-
836
- This is useful for:
837
- - **Learning** how the model uses tools (web search, code interpreter, etc.)
838
- - **Debugging** why the model made certain decisions
839
- - **Understanding** the full context beyond just the final response
840
- - **Transparency** into the model's reasoning process
841
-
842
- ### HTML Output for ERB Templates
514
+ ### HTML Output
843
515
 
844
516
  All display objects have a `to_html` method for rendering in ERB templates:
845
517
 
846
518
  ```erb
847
- <%# Display a chat object %>
848
519
  <%= @chat.to_html %>
849
-
850
- <%# Display individual messages %>
851
- <% @chat.messages.each do |msg| %>
852
- <%= msg.to_html %>
853
- <% end %>
854
-
855
- <%# Display conversation items (quick debug view) %>
856
520
  <%= @chat.get_items.to_html %>
857
521
  ```
858
522
 
859
- The HTML output includes a dark background to match the terminal aesthetic.
860
-
861
- You can also loop over `get_items.data` to build custom displays showing reasoning steps, tool calls, etc.:
862
-
863
- ```erb
864
- <% @chat.get_items.data.each do |item| %>
865
- <% case item.type.to_s %>
866
- <% when "message" %>
867
- <div class="message <%= item.role %>">
868
- <strong><%= item.role.capitalize %>:</strong>
869
- <% if item.content&.first %>
870
- <% content = item.content.first %>
871
- <% if content.type.to_s == "input_text" %>
872
- <%= content.text %>
873
- <% elsif content.type.to_s == "output_text" %>
874
- <%= content.text %>
875
- <% end %>
876
- <% end %>
877
- </div>
878
- <% when "reasoning" %>
879
- <% if item.summary&.first %>
880
- <details class="reasoning">
881
- <summary>Reasoning</summary>
882
- <%= item.summary.first.text %>
883
- </details>
884
- <% end %>
885
- <% when "web_search_call" %>
886
- <% if item.action.respond_to?(:query) && item.action.query %>
887
- <div class="web-search">
888
- Searched: "<%= item.action.query %>"
889
- </div>
890
- <% end %>
891
- <% when "image_generation_call" %>
892
- <div class="image-generation">
893
- Image generated
894
- </div>
895
- <% end %>
896
- <% end %>
897
- ```
898
-
899
- ## Setting messages directly
900
-
901
- You can use `.messages=()` to assign an `Array` of `Hashes` (text-only). Each `Hash` must have keys `:role` and `:content`:
523
+ ## Examples
902
524
 
903
- ```ruby
904
- # Using the planet example with array of hashes
905
- p = AI::Chat.new
525
+ The `examples/` directory contains self-contained scripts demonstrating each feature:
906
526
 
907
- # Set all messages at once instead of calling methods sequentially
908
- p.messages = [
909
- { role: "system", content: "You are a helpful assistant who provides information about planets." },
910
- { role: "user", content: "Tell me about Mars." },
911
- { role: "assistant", content: "Mars is the fourth planet from the Sun...." },
912
- { role: "user", content: "What's the atmosphere like?" },
913
- { role: "assistant", content: "Mars has a very thin atmosphere compared to Earth...." },
914
- { role: "user", content: "Could it support human life?" },
915
- { role: "assistant", content: "Mars currently can't support human life without...." }
916
- ]
527
+ ```bash
528
+ # Run a quick overview (~1 minute)
529
+ bundle exec ruby examples/01_quick.rb
917
530
 
918
- # Now continue the conversation with an API-generated response
919
- p.user("Are there any current missions to go there?")
920
- response = p.generate!
921
- puts response
922
- ```
531
+ # Run all examples
532
+ bundle exec ruby examples/all.rb
923
533
 
924
- For images/files, prefer using `chat.user(..., image:/images:/file:/files:)` so the gem can build the correct multimodal structure.
534
+ # Run any individual example
535
+ bundle exec ruby examples/02_core.rb
536
+ ```
537
+
538
+ | File | Feature |
539
+ |---|---|
540
+ | `01_quick.rb` | Quick overview of key features |
541
+ | `02_core.rb` | Basic chat, messages, and responses |
542
+ | `03_multimodal.rb` | Images and basic file handling |
543
+ | `04_file_handling_comprehensive.rb` | PDFs, text files, Rails uploads |
544
+ | `05_structured_output.rb` | Basic structured output |
545
+ | `06_structured_output_comprehensive.rb` | All supported schema formats |
546
+ | `07_edge_cases.rb` | Error handling and edge cases |
547
+ | `08_additional_patterns.rb` | Direct `add` method, web search + schema |
548
+ | `09_mixed_content.rb` | Combining text and images |
549
+ | `10_image_generation.rb` | Image generation tool |
550
+ | `11_code_interpreter.rb` | Code interpreter tool |
551
+ | `12_background_mode.rb` | Background mode |
552
+ | `13_conversation_features_comprehensive.rb` | Conversation auto-creation and continuity |
553
+ | `14_schema_generation.rb` | Generate schemas from descriptions |
554
+ | `15_proxy.rb` | Proxy support |
555
+ | `16_get_items.rb` | Inspecting conversation items |
556
+ | `17_verbosity.rb` | Verbosity control |
925
557
 
926
558
  ## Contributing
927
559