ai-chat 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/README.md +306 -672
- data/ai-chat.gemspec +3 -3
- data/lib/ai/chat.rb +42 -8
- metadata +5 -5
data/README.md
CHANGED
|
@@ -1,927 +1,561 @@
|
|
|
1
1
|
# AI Chat
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. Add to your Gemfile and install:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
gem "ai-chat", "< 1.0.0"
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
```
|
|
13
|
+
```
|
|
14
|
+
bundle install
|
|
15
|
+
```
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
2. Set up your API key in a `.env` file at the root of your project:
|
|
17
18
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```
|
|
19
|
+
```
|
|
20
|
+
AICHAT_PROXY=true
|
|
21
|
+
AICHAT_PROXY_KEY=your-key-from-prepend-me
|
|
22
|
+
```
|
|
22
23
|
|
|
23
|
-
|
|
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
|
-
|
|
26
|
+
3. Use it:
|
|
50
27
|
|
|
51
|
-
|
|
28
|
+
```ruby
|
|
29
|
+
require "dotenv/load"
|
|
30
|
+
require "ai-chat"
|
|
52
31
|
|
|
53
|
-
|
|
32
|
+
chat = AI::Chat.new
|
|
33
|
+
chat.user("What is Ruby?")
|
|
34
|
+
response = chat.generate!
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
And then, at a command prompt:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
bundle install
|
|
63
|
-
```
|
|
36
|
+
ap response
|
|
37
|
+
```
|
|
64
38
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
- `:role` -- who's speaking (`"system"`, `"user"`, or `"assistant"`)
|
|
46
|
+
- `:content` -- what they said
|
|
74
47
|
|
|
75
|
-
|
|
48
|
+
Here's what a conversation looks like:
|
|
76
49
|
|
|
77
50
|
```ruby
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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.6-terra", ... }
|
|
101
60
|
# }
|
|
102
61
|
|
|
103
|
-
|
|
104
|
-
a.messages
|
|
62
|
+
ap chat.messages
|
|
105
63
|
# => [
|
|
106
64
|
# {
|
|
107
65
|
# :role => "user",
|
|
108
|
-
# :content => "If
|
|
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",
|
|
113
|
-
# :response => { id: "resp_abc...", model: "gpt-5.
|
|
70
|
+
# :content => "Matz is nice and so we are nice.",
|
|
71
|
+
# :response => { id: "resp_abc...", model: "gpt-5.6-terra", ... }
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
- `:role`: who's speaking ("system", "user", or "assistant")
|
|
131
|
-
- `:content`: what they're saying
|
|
78
|
+
This design is intentional:
|
|
132
79
|
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
91
|
+
## Adding Messages
|
|
154
92
|
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
#
|
|
166
|
-
|
|
100
|
+
# Continue the conversation
|
|
101
|
+
chat.user("What about Rails?")
|
|
102
|
+
ap chat.generate!
|
|
103
|
+
```
|
|
167
104
|
|
|
168
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
118
|
+
chat.system("You are helpful")
|
|
119
|
+
chat.add("You are helpful", role: "system")
|
|
200
120
|
|
|
201
121
|
# These are equivalent:
|
|
202
|
-
|
|
203
|
-
|
|
122
|
+
chat.user("Hello!")
|
|
123
|
+
chat.add("Hello!") # role defaults to "user"
|
|
204
124
|
|
|
205
125
|
# These are equivalent:
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
134
|
+
The gem defaults to `gpt-5.6-terra`. You can change it:
|
|
231
135
|
|
|
232
136
|
```ruby
|
|
233
|
-
|
|
234
|
-
|
|
137
|
+
chat = AI::Chat.new
|
|
138
|
+
chat.model = "gpt-5.6-luna"
|
|
235
139
|
```
|
|
236
140
|
|
|
237
|
-
|
|
141
|
+
### API Key
|
|
238
142
|
|
|
239
|
-
|
|
143
|
+
By default, the gem looks for an environment variable based on whether proxy mode is on or off:
|
|
240
144
|
|
|
241
|
-
|
|
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
|
|
150
|
+
You can also specify a custom environment variable name or pass the key directly:
|
|
244
151
|
|
|
245
152
|
```ruby
|
|
246
|
-
|
|
247
|
-
|
|
153
|
+
# Use a different environment variable
|
|
154
|
+
chat = AI::Chat.new(api_key_env_var: "MY_OPENAI_TOKEN")
|
|
248
155
|
|
|
249
|
-
Or
|
|
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
|
-
##
|
|
256
|
-
|
|
257
|
-
You can call `.messages` to get an array containing the conversation so far:
|
|
160
|
+
## Proxy (Prepend.me)
|
|
258
161
|
|
|
259
|
-
|
|
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!
|
|
162
|
+
If you're using a [Prepend.me](https://prepend.me/) proxy key (common in classroom settings), add these to your `.env` file:
|
|
264
163
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
321
|
-
|
|
175
|
+
# Or toggle it on an existing instance
|
|
176
|
+
chat = AI::Chat.new
|
|
177
|
+
chat.proxy = true
|
|
322
178
|
```
|
|
323
179
|
|
|
324
|
-
|
|
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
|
-
|
|
182
|
+
## Web Search
|
|
327
183
|
|
|
328
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
# Single image
|
|
201
|
+
chat.user("What's in this image?", image: "photo.jpg")
|
|
202
|
+
chat.generate!
|
|
399
203
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
204
|
+
# Multiple images
|
|
205
|
+
chat.user("Compare these", images: ["image1.jpg", "image2.jpg"])
|
|
206
|
+
chat.generate!
|
|
403
207
|
```
|
|
404
208
|
|
|
405
|
-
|
|
209
|
+
You can pass local file paths, URLs (`https://...`), or file-like objects (such as `File.open(...)` or Rails uploaded files).
|
|
406
210
|
|
|
407
|
-
|
|
211
|
+
## Including Files
|
|
408
212
|
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
AI::Chat.
|
|
415
|
-
```
|
|
215
|
+
```ruby
|
|
216
|
+
chat = AI::Chat.new
|
|
416
217
|
|
|
417
|
-
|
|
218
|
+
# Single file
|
|
219
|
+
chat.user("Summarize this document", file: "report.pdf")
|
|
220
|
+
chat.generate!
|
|
418
221
|
|
|
419
|
-
|
|
420
|
-
|
|
222
|
+
# Multiple files
|
|
223
|
+
chat.user("Compare these", files: ["doc1.pdf", "doc2.txt"])
|
|
224
|
+
chat.generate!
|
|
421
225
|
```
|
|
422
226
|
|
|
423
|
-
|
|
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
|
-
|
|
229
|
+
You can combine images and files in one message:
|
|
432
230
|
|
|
433
|
-
```
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
267
|
+
When a schema is set, `generate!` returns a parsed Ruby `Hash` with symbolized keys instead of a `String`.
|
|
468
268
|
|
|
469
|
-
|
|
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
|
-
|
|
271
|
+
### Generating a Schema
|
|
474
272
|
|
|
475
|
-
You can
|
|
273
|
+
You can use AI to generate a schema from a plain English description:
|
|
476
274
|
|
|
477
275
|
```ruby
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
281
|
+
## Image Generation
|
|
495
282
|
|
|
496
|
-
|
|
283
|
+
Enable OpenAI's image generation tool to create images from descriptions:
|
|
497
284
|
|
|
498
285
|
```ruby
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
The assistant's message will include an `:images` key with the saved file paths:
|
|
532
299
|
|
|
533
300
|
```ruby
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
|
|
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
|
-
```
|
|
312
|
+
### Configuring the Tool
|
|
580
313
|
|
|
581
|
-
|
|
314
|
+
To configure the tool, pass a `Hash` of options instead of `true`:
|
|
582
315
|
|
|
583
316
|
```ruby
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|
|
604
327
|
|
|
605
|
-
|
|
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.
|
|
329
|
+
|
|
330
|
+
## Code Interpreter
|
|
606
331
|
|
|
607
|
-
|
|
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.
|
|
612
|
-
chat.user("
|
|
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
|
-
|
|
341
|
+
The model will write a Python script, execute it, and return the result (including any generated files like charts).
|
|
621
342
|
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
345
|
+
You can look at the conversation at any point:
|
|
631
346
|
|
|
632
|
-
```
|
|
633
|
-
|
|
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
|
-
|
|
353
|
+
# The return value is the assistant's reply
|
|
354
|
+
response[:content]
|
|
355
|
+
# => "Here's how to boil an egg..."
|
|
637
356
|
|
|
638
|
-
|
|
639
|
-
chat
|
|
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
|
|
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
|
-
|
|
651
|
-
|
|
366
|
+
chat = AI::Chat.new
|
|
367
|
+
chat.system("You are a helpful assistant who provides information about planets.")
|
|
368
|
+
|
|
369
|
+
chat.user("Tell me about Mars.")
|
|
370
|
+
chat.assistant("Mars is the fourth planet from the Sun....")
|
|
652
371
|
|
|
653
|
-
|
|
654
|
-
|
|
372
|
+
chat.user("What's the atmosphere like?")
|
|
373
|
+
chat.assistant("Mars has a very thin atmosphere compared to Earth....")
|
|
655
374
|
|
|
656
|
-
|
|
657
|
-
|
|
375
|
+
# Now continue with an API-generated response
|
|
376
|
+
chat.user("Are there any current missions?")
|
|
377
|
+
chat.generate!
|
|
378
|
+
```
|
|
658
379
|
|
|
659
|
-
|
|
660
|
-
k.assistant("Mars has a very thin atmosphere compared to Earth....")
|
|
380
|
+
You can also set all messages at once with an array of hashes:
|
|
661
381
|
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
]
|
|
664
391
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
response = k.generate!
|
|
668
|
-
puts response
|
|
392
|
+
chat.user("Could it support human life?")
|
|
393
|
+
chat.generate!
|
|
669
394
|
```
|
|
670
395
|
|
|
671
|
-
|
|
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
|
-
|
|
400
|
+
### Reasoning Effort
|
|
674
401
|
|
|
675
|
-
|
|
402
|
+
Control how much reasoning the model does before responding:
|
|
676
403
|
|
|
677
404
|
```ruby
|
|
678
|
-
|
|
679
|
-
|
|
405
|
+
chat = AI::Chat.new
|
|
406
|
+
chat.reasoning_effort = "high" # "none", "low", "medium", "high", "xhigh", or "max"
|
|
680
407
|
|
|
681
|
-
|
|
682
|
-
|
|
408
|
+
chat.user("Explain the tradeoffs between microservices and monoliths.")
|
|
409
|
+
chat.generate!
|
|
683
410
|
```
|
|
684
411
|
|
|
685
|
-
|
|
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 `"none"`, so the model answers directly without spending reasoning tokens. This matters because GPT-5.5 and newer models (including the default `gpt-5.6-terra`) fall back to `"medium"` reasoning when the parameter is omitted, which is slower and more expensive.
|
|
689
413
|
|
|
690
|
-
|
|
414
|
+
Set `reasoning_effort = nil` to omit the reasoning parameter entirely — you will need this if you switch to a model that does not support reasoning (e.g., `gpt-4o`) or one that predates the `"none"` option.
|
|
691
415
|
|
|
692
|
-
|
|
416
|
+
### Verbosity
|
|
693
417
|
|
|
694
|
-
|
|
418
|
+
Control how concise or thorough the model's response is:
|
|
695
419
|
|
|
696
|
-
|
|
697
|
-
|
|
420
|
+
```ruby
|
|
421
|
+
chat = AI::Chat.new
|
|
422
|
+
chat.verbosity = :low # :low, :medium, or :high
|
|
423
|
+
```
|
|
698
424
|
|
|
699
|
-
|
|
425
|
+
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
426
|
|
|
701
|
-
|
|
427
|
+
### Background Mode
|
|
702
428
|
|
|
703
|
-
|
|
429
|
+
Start a response and poll for it later:
|
|
704
430
|
|
|
705
431
|
```ruby
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
# }
|
|
432
|
+
chat = AI::Chat.new
|
|
433
|
+
chat.background = true
|
|
434
|
+
chat.user("Write a detailed analysis of Ruby's GC implementation.")
|
|
435
|
+
chat.generate!
|
|
717
436
|
|
|
718
|
-
#
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
response[:model] # => "gpt-5.2"
|
|
722
|
-
response[:usage] # => {:input_tokens=>5, :output_tokens=>7, :total_tokens=>12}
|
|
437
|
+
# Poll until it completes
|
|
438
|
+
message = chat.get_response(wait: true, timeout: 600)
|
|
439
|
+
puts message[:content]
|
|
723
440
|
```
|
|
724
441
|
|
|
725
|
-
|
|
442
|
+
### Conversation Management
|
|
726
443
|
|
|
727
|
-
-
|
|
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.
|
|
444
|
+
The gem automatically creates a server-side conversation on your first `generate!` call:
|
|
734
445
|
|
|
735
446
|
```ruby
|
|
736
447
|
chat = AI::Chat.new
|
|
737
448
|
chat.user("Hello")
|
|
738
449
|
chat.generate!
|
|
739
450
|
|
|
740
|
-
|
|
451
|
+
chat.conversation_id # => "conv_abc123..."
|
|
741
452
|
|
|
742
|
-
|
|
453
|
+
# The model remembers context across messages
|
|
454
|
+
chat.user("What did I just say?")
|
|
743
455
|
chat.generate!
|
|
744
|
-
|
|
745
|
-
puts chat.last_response_id # => "resp_xyz789..." (a new ID)
|
|
746
456
|
```
|
|
747
457
|
|
|
748
|
-
|
|
458
|
+
You can load an existing conversation:
|
|
749
459
|
|
|
750
460
|
```ruby
|
|
751
|
-
|
|
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)
|
|
461
|
+
chat = AI::Chat.new
|
|
462
|
+
chat.conversation_id = @thread.conversation_id # From your database
|
|
757
463
|
|
|
758
|
-
|
|
464
|
+
chat.user("Continue our discussion")
|
|
465
|
+
chat.generate!
|
|
759
466
|
```
|
|
760
467
|
|
|
761
|
-
###
|
|
468
|
+
### Response Details
|
|
762
469
|
|
|
763
|
-
|
|
470
|
+
Each assistant message includes an API response hash with metadata:
|
|
764
471
|
|
|
765
472
|
```ruby
|
|
766
473
|
chat = AI::Chat.new
|
|
767
|
-
chat.user("Hello")
|
|
474
|
+
chat.user("Hello!")
|
|
768
475
|
chat.generate!
|
|
769
476
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
#
|
|
774
|
-
chat.user("What did I just say?")
|
|
775
|
-
chat.generate! # Uses the same conversation automatically
|
|
477
|
+
response = chat.last[:response]
|
|
478
|
+
response[:id] # => "resp_abc123..."
|
|
479
|
+
response[:model] # => "gpt-5.6-terra"
|
|
480
|
+
response[:usage] # => { input_tokens: 5, output_tokens: 7, total_tokens: 12 }
|
|
776
481
|
```
|
|
777
482
|
|
|
778
|
-
|
|
483
|
+
The `last_response_id` reader always holds the most recent response ID:
|
|
779
484
|
|
|
780
485
|
```ruby
|
|
781
|
-
#
|
|
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
|
|
486
|
+
chat.last_response_id # => "resp_abc123..."
|
|
787
487
|
```
|
|
788
488
|
|
|
789
|
-
|
|
489
|
+
### Inspecting Conversation Items
|
|
790
490
|
|
|
791
|
-
The `get_items` method fetches all conversation items
|
|
491
|
+
The `get_items` method fetches all conversation items from the API, including messages, tool calls, reasoning steps, and web searches:
|
|
792
492
|
|
|
793
493
|
```ruby
|
|
794
494
|
chat = AI::Chat.new
|
|
795
|
-
chat.reasoning_effort = "high"
|
|
495
|
+
chat.reasoning_effort = "high"
|
|
796
496
|
chat.web_search = true
|
|
797
497
|
chat.user("Search for Ruby tutorials")
|
|
798
498
|
chat.generate!
|
|
799
499
|
|
|
800
|
-
#
|
|
500
|
+
# Pretty-printed in IRB/console
|
|
801
501
|
chat.get_items
|
|
802
502
|
|
|
803
|
-
#
|
|
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
|
|
503
|
+
# Iterate programmatically
|
|
813
504
|
chat.get_items.data.each do |item|
|
|
814
505
|
case item.type
|
|
815
506
|
when :message
|
|
816
507
|
puts "#{item.role}: #{item.content.first.text}"
|
|
817
508
|
when :web_search_call
|
|
818
|
-
puts "
|
|
509
|
+
puts "Searched: #{item.action.query}" if item.action.respond_to?(:query)
|
|
819
510
|
when :reasoning
|
|
820
|
-
|
|
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
|
|
511
|
+
puts "Reasoning: #{item.summary.first.text}" if item.summary&.first
|
|
826
512
|
end
|
|
827
513
|
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
514
|
```
|
|
833
515
|
|
|
834
|
-
|
|
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
|
|
516
|
+
### HTML Output
|
|
843
517
|
|
|
844
518
|
All display objects have a `to_html` method for rendering in ERB templates:
|
|
845
519
|
|
|
846
520
|
```erb
|
|
847
|
-
<%# Display a chat object %>
|
|
848
521
|
<%= @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
522
|
<%= @chat.get_items.to_html %>
|
|
857
523
|
```
|
|
858
524
|
|
|
859
|
-
|
|
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`:
|
|
525
|
+
## Examples
|
|
902
526
|
|
|
903
|
-
|
|
904
|
-
# Using the planet example with array of hashes
|
|
905
|
-
p = AI::Chat.new
|
|
527
|
+
The `examples/` directory contains self-contained scripts demonstrating each feature:
|
|
906
528
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
]
|
|
529
|
+
```bash
|
|
530
|
+
# Run a quick overview (~1 minute)
|
|
531
|
+
bundle exec ruby examples/01_quick.rb
|
|
917
532
|
|
|
918
|
-
#
|
|
919
|
-
|
|
920
|
-
response = p.generate!
|
|
921
|
-
puts response
|
|
922
|
-
```
|
|
533
|
+
# Run all examples
|
|
534
|
+
bundle exec ruby examples/all.rb
|
|
923
535
|
|
|
924
|
-
|
|
536
|
+
# Run any individual example
|
|
537
|
+
bundle exec ruby examples/02_core.rb
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
| File | Feature |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `01_quick.rb` | Quick overview of key features |
|
|
543
|
+
| `02_core.rb` | Basic chat, messages, and responses |
|
|
544
|
+
| `03_multimodal.rb` | Images and basic file handling |
|
|
545
|
+
| `04_file_handling_comprehensive.rb` | PDFs, text files, Rails uploads |
|
|
546
|
+
| `05_structured_output.rb` | Basic structured output |
|
|
547
|
+
| `06_structured_output_comprehensive.rb` | All supported schema formats |
|
|
548
|
+
| `07_edge_cases.rb` | Error handling and edge cases |
|
|
549
|
+
| `08_additional_patterns.rb` | Direct `add` method, web search + schema |
|
|
550
|
+
| `09_mixed_content.rb` | Combining text and images |
|
|
551
|
+
| `10_image_generation.rb` | Image generation tool |
|
|
552
|
+
| `11_code_interpreter.rb` | Code interpreter tool |
|
|
553
|
+
| `12_background_mode.rb` | Background mode |
|
|
554
|
+
| `13_conversation_features_comprehensive.rb` | Conversation auto-creation and continuity |
|
|
555
|
+
| `14_schema_generation.rb` | Generate schemas from descriptions |
|
|
556
|
+
| `15_proxy.rb` | Proxy support |
|
|
557
|
+
| `16_get_items.rb` | Inspecting conversation items |
|
|
558
|
+
| `17_verbosity.rb` | Verbosity control |
|
|
925
559
|
|
|
926
560
|
## Contributing
|
|
927
561
|
|