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.
- checksums.yaml +4 -4
- data/README.md +304 -672
- data/ai-chat.gemspec +2 -2
- data/lib/ai/chat.rb +31 -4
- metadata +3 -3
data/README.md
CHANGED
|
@@ -1,927 +1,559 @@
|
|
|
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.2", ... }
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
chat = AI::Chat.new
|
|
97
|
+
chat.user("Hello!")
|
|
98
|
+
ap chat.generate!
|
|
161
99
|
|
|
162
|
-
#
|
|
163
|
-
|
|
100
|
+
# Continue the conversation
|
|
101
|
+
chat.user("What about Rails?")
|
|
102
|
+
ap chat.generate!
|
|
103
|
+
```
|
|
164
104
|
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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.2`. You can change it:
|
|
231
135
|
|
|
232
136
|
```ruby
|
|
233
|
-
|
|
234
|
-
|
|
137
|
+
chat = AI::Chat.new
|
|
138
|
+
chat.model = "gpt-4o"
|
|
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
|
-
##
|
|
160
|
+
## Proxy (Prepend.me)
|
|
256
161
|
|
|
257
|
-
|
|
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
|
-
|
|
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"]
|
|
312
|
+
### Configuring the Tool
|
|
575
313
|
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
599
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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.")
|
|
652
368
|
|
|
653
|
-
|
|
654
|
-
|
|
369
|
+
chat.user("Tell me about Mars.")
|
|
370
|
+
chat.assistant("Mars is the fourth planet from the Sun....")
|
|
655
371
|
|
|
656
|
-
|
|
657
|
-
|
|
372
|
+
chat.user("What's the atmosphere like?")
|
|
373
|
+
chat.assistant("Mars has a very thin atmosphere compared to Earth....")
|
|
658
374
|
|
|
659
|
-
|
|
660
|
-
|
|
375
|
+
# Now continue with an API-generated response
|
|
376
|
+
chat.user("Are there any current missions?")
|
|
377
|
+
chat.generate!
|
|
378
|
+
```
|
|
661
379
|
|
|
662
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
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" # "low", "medium", or "high"
|
|
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 `nil` (no reasoning parameter is sent). For `gpt-5.2`, this is equivalent to no reasoning.
|
|
689
413
|
|
|
690
|
-
|
|
414
|
+
### Verbosity
|
|
691
415
|
|
|
692
|
-
|
|
416
|
+
Control how concise or thorough the model's response is:
|
|
693
417
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
418
|
+
```ruby
|
|
419
|
+
chat = AI::Chat.new
|
|
420
|
+
chat.verbosity = :low # :low, :medium, or :high
|
|
421
|
+
```
|
|
698
422
|
|
|
699
|
-
|
|
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
|
-
|
|
425
|
+
### Background Mode
|
|
702
426
|
|
|
703
|
-
|
|
427
|
+
Start a response and poll for it later:
|
|
704
428
|
|
|
705
429
|
```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
|
-
# }
|
|
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
|
-
#
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
440
|
+
### Conversation Management
|
|
726
441
|
|
|
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.
|
|
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
|
-
|
|
449
|
+
chat.conversation_id # => "conv_abc123..."
|
|
741
450
|
|
|
742
|
-
|
|
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
|
-
|
|
456
|
+
You can load an existing conversation:
|
|
749
457
|
|
|
750
458
|
```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)
|
|
459
|
+
chat = AI::Chat.new
|
|
460
|
+
chat.conversation_id = @thread.conversation_id # From your database
|
|
757
461
|
|
|
758
|
-
|
|
462
|
+
chat.user("Continue our discussion")
|
|
463
|
+
chat.generate!
|
|
759
464
|
```
|
|
760
465
|
|
|
761
|
-
###
|
|
466
|
+
### Response Details
|
|
762
467
|
|
|
763
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
#
|
|
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
|
-
|
|
481
|
+
The `last_response_id` reader always holds the most recent response ID:
|
|
779
482
|
|
|
780
483
|
```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
|
|
484
|
+
chat.last_response_id # => "resp_abc123..."
|
|
787
485
|
```
|
|
788
486
|
|
|
789
|
-
|
|
487
|
+
### Inspecting Conversation Items
|
|
790
488
|
|
|
791
|
-
The `get_items` method fetches all conversation items
|
|
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"
|
|
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
|
-
#
|
|
498
|
+
# Pretty-printed in IRB/console
|
|
801
499
|
chat.get_items
|
|
802
500
|
|
|
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
|
|
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 "
|
|
507
|
+
puts "Searched: #{item.action.query}" if item.action.respond_to?(:query)
|
|
819
508
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
]
|
|
527
|
+
```bash
|
|
528
|
+
# Run a quick overview (~1 minute)
|
|
529
|
+
bundle exec ruby examples/01_quick.rb
|
|
917
530
|
|
|
918
|
-
#
|
|
919
|
-
|
|
920
|
-
response = p.generate!
|
|
921
|
-
puts response
|
|
922
|
-
```
|
|
531
|
+
# Run all examples
|
|
532
|
+
bundle exec ruby examples/all.rb
|
|
923
533
|
|
|
924
|
-
|
|
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
|
|