ai-chat 0.6.0 → 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 +304 -672
  3. data/ai-chat.gemspec +2 -2
  4. data/lib/ai/chat.rb +31 -4
  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
5
+ ## Quick Start
6
6
 
7
- This gem includes comprehensive example scripts that showcase all features and serve as both documentation and validation tests. To explore the capabilities:
7
+ 1. Add to your Gemfile and install:
8
8
 
9
- ### Quick Start
9
+ ```ruby
10
+ gem "ai-chat", "< 1.0.0"
11
+ ```
10
12
 
11
- ```bash
12
- # Run a quick overview of key features (takes ~1 minute)
13
- bundle exec ruby examples/01_quick.rb
14
- ```
13
+ ```
14
+ bundle install
15
+ ```
15
16
 
16
- ### Run All Examples
17
+ 2. Set up your API key in a `.env` file at the root of your project:
17
18
 
18
- ```bash
19
- # Run the complete test suite demonstrating all features
20
- bundle exec ruby examples/all.rb
21
- ```
19
+ ```
20
+ AICHAT_PROXY=true
21
+ AICHAT_PROXY_KEY=your-key-from-prepend-me
22
+ ```
22
23
 
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
- ```
24
+ (If you have your own OpenAI account, you can skip proxy mode and set `OPENAI_API_KEY` instead.)
48
25
 
49
- ## Installation
26
+ 3. Use it:
50
27
 
51
- ### Gemfile way (preferred)
28
+ ```ruby
29
+ require "dotenv/load"
30
+ require "ai-chat"
52
31
 
53
- Add this line to your application's Gemfile:
32
+ chat = AI::Chat.new
33
+ chat.user("What is Ruby?")
34
+ response = chat.generate!
54
35
 
55
- ```ruby
56
- gem "ai-chat", "< 1.0.0"
57
- ```
58
-
59
- And then, at a command prompt:
60
-
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
96
+ chat = AI::Chat.new
97
+ chat.user("Hello!")
98
+ ap chat.generate!
161
99
 
162
- # Add system instructions
163
- b.add("You are a helpful assistant that talks like Shakespeare.", role: "system")
100
+ # Continue the conversation
101
+ chat.user("What about Rails?")
102
+ ap chat.generate!
103
+ ```
164
104
 
165
- # Add a user message (role defaults to "user")
166
- b.add("If the Ruby community had an official motto, what might it be?")
105
+ You can also add system instructions (to guide the model's behavior) and manually add assistant messages (to reconstruct past conversations):
167
106
 
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
- # ]
180
-
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
- By default, the gem uses `OPENAI_API_KEY`. When proxy mode is enabled (`AICHAT_PROXY=true`), it uses `AICHAT_PROXY_KEY` instead.
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!
264
-
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
- # ]
281
-
282
- # Get just the last response
283
- h.messages.last[:content]
284
- # => "Here's how to boil an egg..."
285
-
286
- # Or use the convenient shortcut
287
- h.last[:content]
288
- # => "Here's how to boil an egg..."
289
164
  ```
290
-
291
- ## Web Search
292
-
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.
294
-
295
- ```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
165
+ AICHAT_PROXY=true
166
+ AICHAT_PROXY_KEY=your-key-from-prepend-me
300
167
  ```
301
168
 
302
- ## Structured Output
303
-
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)):
169
+ You can also enable proxy mode in code:
305
170
 
306
171
  ```ruby
307
- i = AI::Chat.new
308
-
309
- i.system("You are an expert nutritionist. The user will describe a meal. Estimate the calories, carbs, fat, and protein.")
310
-
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}}'
313
-
314
- i.user("1 slice of pizza")
315
-
316
- response = i.generate!
317
- data = response[:content]
318
- # => {:fat=>15, :protein=>12, :carbs=>35, :total_calories=>285}
172
+ # At construction time
173
+ chat = AI::Chat.new(proxy: true)
319
174
 
320
- # The response is parsed JSON, not a string!
321
- data[:total_calories] # => 285
175
+ # Or toggle it on an existing instance
176
+ chat = AI::Chat.new
177
+ chat.proxy = true
322
178
  ```
323
179
 
324
- ### Schema Formats
180
+ When proxy is enabled, API calls are routed through Prepend.me, and the gem uses `AICHAT_PROXY_KEY` instead of `OPENAI_API_KEY`.
325
181
 
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.
182
+ ## Web Search
327
183
 
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
- }
347
- ```
184
+ Give the model access to current information from the internet:
348
185
 
349
- #### 2. Schema with `name`, `strict`, and `schema` Keys
350
186
  ```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
- }
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!
365
191
  ```
366
192
 
367
- #### 3. Simple JSON Schema Object
368
- ```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 = {
372
- type: "object",
373
- properties: {
374
- fat: { type: "number", description: "The amount of fat in grams." },
375
- protein: { type: "number", description: "The amount of protein in grams." }
376
- },
377
- required: ["fat", "protein"],
378
- additionalProperties: false
379
- }
380
- ```
193
+ ## Including Images
381
194
 
382
- #### 4. JSON String Formats
383
- All the above formats also work as JSON strings:
195
+ Use the `image:` or `images:` parameter to send images along with your message:
384
196
 
385
197
  ```ruby
386
- # As a JSON string with full format
387
- i.schema = '{"format":{"type":"json_schema","name":"nutrition_values","strict":true,"schema":{...}}}'
388
-
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
198
+ chat = AI::Chat.new
397
199
 
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.
200
+ # Single image
201
+ chat.user("What's in this image?", image: "photo.jpg")
202
+ chat.generate!
399
203
 
400
- ```rb
401
- AI::Chat.generate_schema!("A user profile with name (required), email (required), age (number), and bio (optional text).")
402
- # => "{ ... }"
204
+ # Multiple images
205
+ chat.user("Compare these", images: ["image1.jpg", "image2.jpg"])
206
+ chat.generate!
403
207
  ```
404
208
 
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.
209
+ You can pass local file paths, URLs (`https://...`), or file-like objects (such as `File.open(...)` or Rails uploaded files).
406
210
 
407
- This class method uses the same API key and proxy resolution as `AI::Chat.new`. You can also pass the API key directly or choose a different environment variable:
211
+ ## Including Files
408
212
 
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")
213
+ Use the `file:` or `files:` parameter to send documents:
412
214
 
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
- ```
215
+ ```ruby
216
+ chat = AI::Chat.new
416
217
 
417
- `generate_schema!` also follows proxy defaults from the `AICHAT_PROXY` environment variable.
218
+ # Single file
219
+ chat.user("Summarize this document", file: "report.pdf")
220
+ chat.generate!
418
221
 
419
- ```bash
420
- export AICHAT_PROXY=true
222
+ # Multiple files
223
+ chat.user("Compare these", files: ["doc1.pdf", "doc2.txt"])
224
+ chat.generate!
421
225
  ```
422
226
 
423
- If you pass `proxy: true` or `proxy: false`, that explicit value overrides the env default.
424
-
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
- ```
227
+ PDFs are sent as attachments. Text-based files have their content extracted and sent as text.
430
228
 
431
- If you don't want to write the output to a file, you can pass `false` to `location`.
229
+ You can combine images and files in one message:
432
230
 
433
- ```rb
434
- AI::Chat.generate_schema!("A user with full name (required), first_name (required), and last_name (required).", location: false)
435
- # => { ... }
231
+ ```ruby
232
+ chat.user("Analyze these materials",
233
+ images: ["chart1.png", "chart2.png"],
234
+ files: ["report.pdf", "data.csv"])
235
+ chat.generate!
436
236
  ```
437
237
 
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
238
+ ## Structured Output
445
239
 
446
- You can include images in your chat messages using the `user` method with the `image` or `images` parameter:
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:
447
241
 
448
242
  ```ruby
449
- j = AI::Chat.new
243
+ chat = AI::Chat.new
244
+ chat.system("You are an expert nutritionist. Estimate the nutritional content of the meal the user describes.")
450
245
 
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..."
246
+ chat.schema = {
247
+ type: "object",
248
+ properties: {
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" }
253
+ },
254
+ required: ["fat", "protein", "carbs", "calories"],
255
+ additionalProperties: false
256
+ }
257
+
258
+ chat.user("1 slice of pizza")
259
+ response = chat.generate!
454
260
 
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..."
261
+ data = response[:content]
262
+ # => { fat: 15, protein: 12, carbs: 35, calories: 285 }
458
263
 
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!
264
+ data[:calories] # => 285
465
265
  ```
466
266
 
467
- The gem supports three types of image inputs:
267
+ When a schema is set, `generate!` returns a parsed Ruby `Hash` with symbolized keys instead of a `String`.
468
268
 
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)
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.
472
270
 
473
- ## Including Files
271
+ ### Generating a Schema
474
272
 
475
- You can include files (PDFs, text files, etc.) in your messages using the `file` or `files` parameter:
273
+ You can use AI to generate a schema from a plain English description:
476
274
 
477
275
  ```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!
276
+ AI::Chat.generate_schema!("A user profile with name (required), email (required), age (number), and bio (optional).")
487
277
  ```
488
278
 
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
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.
493
280
 
494
- ## Mixed Content (Images + Files)
281
+ ## Image Generation
495
282
 
496
- You can send images and files together in a single message:
283
+ Enable OpenAI's image generation tool to create images from descriptions:
497
284
 
498
285
  ```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!
286
+ chat = AI::Chat.new
287
+ chat.image_generation = true
288
+ chat.user("Draw a picture of a kitten")
289
+ chat.generate!
512
290
  ```
513
291
 
514
- **Note**: Images should use `image:`/`images:` parameters, while documents should use `file:`/`files:` parameters.
515
-
516
- ## Image generation
517
-
518
- You can enable OpenAI's image generation tool:
292
+ Generated images are saved to `./images` by default (in timestamped subfolders like `./images/20250804T113039_resp_abc123/001.png`). You can change the folder:
519
293
 
520
294
  ```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
- # }
295
+ chat.image_folder = "./my_images"
529
296
  ```
530
297
 
531
- By default, images are saved to `./images`. You can configure a different location:
298
+ The assistant's message will include an `:images` key with the saved file paths:
532
299
 
533
300
  ```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
- # }
301
+ chat.last[:images]
302
+ # => ["./images/20250804T113039_resp_abc123/001.png"]
543
303
  ```
544
304
 
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)
548
-
549
- The folder structure ensures images are organized chronologically and by response.
550
-
551
- The messages array will now look like this:
305
+ AI-generated images are stored by OpenAI, so you can refine them in follow-up messages without re-sending:
552
306
 
553
307
  ```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
- # ]
308
+ chat.user("Make it even cuter")
309
+ chat.generate!
567
310
  ```
568
311
 
569
- You can access the image filenames in several ways:
570
-
571
- ```ruby
572
- # From the last message
573
- images = a.messages.last[:images]
574
- # => ["./images/20250804T11303912_resp_abc123/001.png"]
312
+ ### Configuring the Tool
575
313
 
576
- # From the response object
577
- images = a.messages.last.dig(:response, :images)
578
- # => ["./images/20250804T11303912_resp_abc123/001.png"]
579
- ```
580
-
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.
314
+ To configure the tool, pass a `Hash` of options instead of `true`:
582
315
 
583
316
  ```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:", ... }
317
+ chat.image_generation = {
318
+ size: "1536x1024",
319
+ quality: "low",
320
+ model: "gpt-image-2"
321
+ }
593
322
  ```
594
323
 
595
- ## Code Interpreter
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.
596
325
 
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.", ... }
603
- ```
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`.
327
+
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.
604
329
 
605
- ## Background mode
330
+ ## Code Interpreter
606
331
 
607
- If you want to start a response and poll for it later, set `background = true` before calling `generate!`:
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:
608
333
 
609
334
  ```ruby
610
335
  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.")
336
+ chat.code_interpreter = true
337
+ chat.user("Plot y = 2x^3 for x from -5 to 5")
613
338
  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
339
  ```
619
340
 
620
- ## Proxying Through Prepend.me
621
-
622
- You can proxy API calls through [Prepend.me](https://prepend.me/). When proxy mode is enabled, the gem uses the `AICHAT_PROXY_KEY` environment variable instead of `OPENAI_API_KEY`.
341
+ The model will write a Python script, execute it, and return the result (including any generated files like charts).
623
342
 
624
- You can enable proxy mode at construction time:
625
-
626
- ```rb
627
- chat = AI::Chat.new(proxy: true)
628
- ```
343
+ ## Inspecting Your Conversation
629
344
 
630
- Or default it from the environment (case-insensitive):
345
+ You can look at the conversation at any point:
631
346
 
632
- ```bash
633
- export AICHAT_PROXY=true
634
- ```
347
+ ```ruby
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!
635
352
 
636
- Or toggle it on an existing instance:
353
+ # The return value is the assistant's reply
354
+ response[:content]
355
+ # => "Here's how to boil an egg..."
637
356
 
638
- ```rb
639
- chat = AI::Chat.new
640
- chat.proxy = true
357
+ # See the whole conversation
358
+ ap chat.messages
641
359
  ```
642
360
 
643
- When proxy is enabled, **you must set `AICHAT_PROXY_KEY`** with your API key from Prepend.me.
644
-
645
361
  ## Building Conversations Without API Calls
646
362
 
647
- You can manually add assistant messages without making API calls, which is useful when reconstructing a past conversation:
363
+ You can manually build up a conversation without calling the API, which is useful for reconstructing a past conversation from your database:
648
364
 
649
365
  ```ruby
650
- # Create a new chat instance
651
- k = AI::Chat.new
366
+ chat = AI::Chat.new
367
+ chat.system("You are a helpful assistant who provides information about planets.")
652
368
 
653
- # Add previous messages
654
- k.system("You are a helpful assistant who provides information about planets.")
369
+ chat.user("Tell me about Mars.")
370
+ chat.assistant("Mars is the fourth planet from the Sun....")
655
371
 
656
- k.user("Tell me about Mars.")
657
- k.assistant("Mars is the fourth planet from the Sun....")
372
+ chat.user("What's the atmosphere like?")
373
+ chat.assistant("Mars has a very thin atmosphere compared to Earth....")
658
374
 
659
- k.user("What's the atmosphere like?")
660
- k.assistant("Mars has a very thin atmosphere compared to Earth....")
375
+ # Now continue with an API-generated response
376
+ chat.user("Are there any current missions?")
377
+ chat.generate!
378
+ ```
661
379
 
662
- k.user("Could it support human life?")
663
- k.assistant("Mars currently can't support human life without....")
380
+ You can also set all messages at once with an array of hashes:
664
381
 
665
- # Now continue the conversation with an API-generated response
666
- k.user("Are there any current missions to go there?")
667
- response = k.generate!
668
- puts response
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!
669
394
  ```
670
395
 
671
- 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.
396
+ For messages with images or files, use `chat.user(..., image:, file:)` instead so the gem can build the correct multimodal structure.
397
+
398
+ ## Advanced
672
399
 
673
- ## Reasoning Effort
400
+ ### Reasoning Effort
674
401
 
675
- You can control how much reasoning the model does before producing its response:
402
+ Control how much reasoning the model does before responding:
676
403
 
677
404
  ```ruby
678
- l = AI::Chat.new
679
- l.reasoning_effort = "low" # Can be "low", "medium", or "high"
405
+ chat = AI::Chat.new
406
+ chat.reasoning_effort = "high" # "low", "medium", or "high"
680
407
 
681
- l.user("What does this error message mean? <insert error message>")
682
- l.generate!
408
+ chat.user("Explain the tradeoffs between microservices and monoliths.")
409
+ chat.generate!
683
410
  ```
684
411
 
685
- The `reasoning_effort` parameter guides the model on how many reasoning tokens to generate. Options are:
686
- - `"low"`: Favors speed and economical token usage.
687
- - `"medium"`: Balances speed and reasoning accuracy.
688
- - `"high"`: Favors more complete reasoning.
412
+ By default, `reasoning_effort` is `nil` (no reasoning parameter is sent). For `gpt-5.2`, this is equivalent to no reasoning.
689
413
 
690
- 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.
414
+ ### Verbosity
691
415
 
692
- ## Verbosity
416
+ Control how concise or thorough the model's response is:
693
417
 
694
- 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:
695
-
696
- - High verbosity: Use when you need the model to provide thorough explanations of documents or perform extensive code refactoring.
697
- - Low verbosity: Best for situations where you want concise answers or simple code generation, such as SQL queries.
418
+ ```ruby
419
+ chat = AI::Chat.new
420
+ chat.verbosity = :low # :low, :medium, or :high
421
+ ```
698
422
 
699
- 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`**.
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`.
700
424
 
701
- ## Advanced: Response Details
425
+ ### Background Mode
702
426
 
703
- 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:
704
428
 
705
429
  ```ruby
706
- t = AI::Chat.new
707
- t.user("Hello!")
708
- t.generate!
709
-
710
- # Each assistant message includes a response object
711
- t.messages.last
712
- # => {
713
- # :role => "assistant",
714
- # :content => "Hello! How can I help you today?",
715
- # :response => { id: "resp_abc...", model: "gpt-5.2", ... }
716
- # }
430
+ chat = AI::Chat.new
431
+ chat.background = true
432
+ chat.user("Write a detailed analysis of Ruby's GC implementation.")
433
+ chat.generate!
717
434
 
718
- # Access detailed information
719
- response = t.last[:response]
720
- response[:id] # => "resp_abc123..."
721
- response[:model] # => "gpt-5.2"
722
- 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]
723
438
  ```
724
439
 
725
- This information is useful for:
440
+ ### Conversation Management
726
441
 
727
- - Debugging and monitoring token usage.
728
- - Understanding which model was actually used.
729
- - Future features like cost tracking.
730
-
731
- ### Last Response ID
732
-
733
- 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:
734
443
 
735
444
  ```ruby
736
445
  chat = AI::Chat.new
737
446
  chat.user("Hello")
738
447
  chat.generate!
739
448
 
740
- puts chat.last_response_id # => "resp_abc123..."
449
+ chat.conversation_id # => "conv_abc123..."
741
450
 
742
- chat.user("Goodbye")
451
+ # The model remembers context across messages
452
+ chat.user("What did I just say?")
743
453
  chat.generate!
744
-
745
- puts chat.last_response_id # => "resp_xyz789..." (a new ID)
746
454
  ```
747
455
 
748
- 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:
749
457
 
750
458
  ```ruby
751
- require "openai"
752
-
753
- client = OpenAI::Client.new(api_key: ENV.fetch("OPENAI_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