ai-chat 0.4.0 → 0.5.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 +220 -141
- data/ai-chat.gemspec +3 -3
- data/lib/ai/amazing_print.rb +25 -26
- data/lib/ai/chat.rb +60 -72
- data/lib/ai/items.rb +54 -0
- data/lib/ai/message.rb +23 -0
- data/lib/ai-chat.rb +11 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ece58b865d212ce788931ee3e7322b457bbe6ed8495e4374b55ecf697b44e55c
|
|
4
|
+
data.tar.gz: dd4bf5e24a5190eba63c662d93dcaaad24886b7f16cfbec5aebc37a9f3530cd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcb3587c00724b1da3d04adb2b9a7413225680b80971bd08db127edab6d7f18e52958b7511f3aa7e13cb95f40961f8889cae3f0cc892913930547d5a7887d452
|
|
7
|
+
data.tar.gz: e831f86fecfa31cf28df66910fc6ca98c66a9ad8290709431a0e5e9b8a6e31965ba7af9d5a00dbcf3f907427abe8901abf23a1cd136b26bc2ebffe260b6334ed
|
data/README.md
CHANGED
|
@@ -39,6 +39,7 @@ The `examples/` directory contains focused examples for specific features:
|
|
|
39
39
|
- `13_conversation_features_comprehensive.rb` - Conversation features (auto-creation, continuity, inspection)
|
|
40
40
|
- `14_schema_generation.rb` - Generate JSON schemas from natural language
|
|
41
41
|
- `15_proxy.rb` - Proxy support for student accounts
|
|
42
|
+
- `16_get_items.rb` - Inspecting conversation items (reasoning, web searches, image generation)
|
|
42
43
|
|
|
43
44
|
Each example is self-contained and can be run individually:
|
|
44
45
|
```bash
|
|
@@ -83,22 +84,44 @@ a = AI::Chat.new
|
|
|
83
84
|
a.add("If the Ruby community had an official motto, what might it be?")
|
|
84
85
|
|
|
85
86
|
# See the convo so far - it's just an array of hashes!
|
|
86
|
-
|
|
87
|
-
# => [
|
|
87
|
+
a.messages
|
|
88
|
+
# => [
|
|
89
|
+
# {
|
|
90
|
+
# :role => "user",
|
|
91
|
+
# :content => "If the Ruby community had an official motto, what might it be?"
|
|
92
|
+
# }
|
|
93
|
+
# ]
|
|
88
94
|
|
|
89
95
|
# Generate the next message using AI
|
|
90
|
-
a.generate!
|
|
96
|
+
a.generate!
|
|
97
|
+
# => {
|
|
98
|
+
# :role => "assistant",
|
|
99
|
+
# :content => "Matz is nice and so we are nice",
|
|
100
|
+
# :response => { ... }
|
|
101
|
+
# }
|
|
91
102
|
|
|
92
103
|
# Your array now includes the assistant's response
|
|
93
|
-
|
|
104
|
+
a.messages
|
|
94
105
|
# => [
|
|
95
|
-
#
|
|
96
|
-
#
|
|
106
|
+
# {
|
|
107
|
+
# :role => "user",
|
|
108
|
+
# :content => "If the Ruby community had an official motto, what might it be?"
|
|
109
|
+
# },
|
|
110
|
+
# {
|
|
111
|
+
# :role => "assistant",
|
|
112
|
+
# :content => "Matz is nice and so we are nice",
|
|
113
|
+
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
|
|
114
|
+
# }
|
|
97
115
|
# ]
|
|
98
116
|
|
|
99
117
|
# Continue the conversation
|
|
100
118
|
a.add("What about Rails?")
|
|
101
|
-
a.generate!
|
|
119
|
+
a.generate!
|
|
120
|
+
# => {
|
|
121
|
+
# :role => "assistant",
|
|
122
|
+
# :content => "Convention over configuration.",
|
|
123
|
+
# :response => { ... }
|
|
124
|
+
# }
|
|
102
125
|
```
|
|
103
126
|
|
|
104
127
|
## Understanding the Data Structure
|
|
@@ -111,9 +134,19 @@ That's it! You're building something like this:
|
|
|
111
134
|
|
|
112
135
|
```ruby
|
|
113
136
|
[
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
150
|
]
|
|
118
151
|
```
|
|
119
152
|
|
|
@@ -133,14 +166,25 @@ b.add("You are a helpful assistant that talks like Shakespeare.", role: "system"
|
|
|
133
166
|
b.add("If the Ruby community had an official motto, what might it be?")
|
|
134
167
|
|
|
135
168
|
# Check what we've built
|
|
136
|
-
|
|
169
|
+
b.messages
|
|
137
170
|
# => [
|
|
138
|
-
#
|
|
139
|
-
#
|
|
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
|
+
# }
|
|
140
179
|
# ]
|
|
141
180
|
|
|
142
181
|
# Generate a response
|
|
143
|
-
b.generate!
|
|
182
|
+
b.generate!
|
|
183
|
+
# => {
|
|
184
|
+
# :role => "assistant",
|
|
185
|
+
# :content => "Methinks 'tis 'Ruby doth bring joy to all who craft with care'",
|
|
186
|
+
# :response => { ... }
|
|
187
|
+
# }
|
|
144
188
|
```
|
|
145
189
|
|
|
146
190
|
### Convenience Methods
|
|
@@ -183,7 +227,7 @@ d.generate! # Generate a response
|
|
|
183
227
|
|
|
184
228
|
### Model
|
|
185
229
|
|
|
186
|
-
By default, the gem uses OpenAI's `gpt-5.
|
|
230
|
+
By default, the gem uses OpenAI's `gpt-5.2` model. If you want to use a different model, you can set it:
|
|
187
231
|
|
|
188
232
|
```ruby
|
|
189
233
|
e = AI::Chat.new
|
|
@@ -219,11 +263,20 @@ h.user("How do I boil an egg?")
|
|
|
219
263
|
h.generate!
|
|
220
264
|
|
|
221
265
|
# See the whole conversation
|
|
222
|
-
|
|
266
|
+
h.messages
|
|
223
267
|
# => [
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
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
|
+
# }
|
|
227
280
|
# ]
|
|
228
281
|
|
|
229
282
|
# Get just the last response
|
|
@@ -460,7 +513,11 @@ You can enable OpenAI's image generation tool:
|
|
|
460
513
|
a = AI::Chat.new
|
|
461
514
|
a.image_generation = true
|
|
462
515
|
a.user("Draw a picture of a kitten")
|
|
463
|
-
a.generate!
|
|
516
|
+
a.generate!
|
|
517
|
+
# => {
|
|
518
|
+
# :content => "Here is your picture of a kitten:",
|
|
519
|
+
# :response => { ... }
|
|
520
|
+
# }
|
|
464
521
|
```
|
|
465
522
|
|
|
466
523
|
By default, images are saved to `./images`. You can configure a different location:
|
|
@@ -470,7 +527,11 @@ a = AI::Chat.new
|
|
|
470
527
|
a.image_generation = true
|
|
471
528
|
a.image_folder = "./my_images"
|
|
472
529
|
a.user("Draw a picture of a kitten")
|
|
473
|
-
a.generate!
|
|
530
|
+
a.generate!
|
|
531
|
+
# => {
|
|
532
|
+
# :content => "Here is your picture of a kitten:",
|
|
533
|
+
# :response => { ... }
|
|
534
|
+
# }
|
|
474
535
|
```
|
|
475
536
|
|
|
476
537
|
Images are saved in timestamped subfolders using ISO 8601 basic format. For example:
|
|
@@ -482,11 +543,19 @@ The folder structure ensures images are organized chronologically and by respons
|
|
|
482
543
|
The messages array will now look like this:
|
|
483
544
|
|
|
484
545
|
```ruby
|
|
485
|
-
|
|
546
|
+
a.messages
|
|
486
547
|
# => [
|
|
487
|
-
#
|
|
488
|
-
#
|
|
489
|
-
#
|
|
548
|
+
# {
|
|
549
|
+
# :role => "user",
|
|
550
|
+
# :content => "Draw a picture of a kitten"
|
|
551
|
+
# },
|
|
552
|
+
# {
|
|
553
|
+
# :role => "assistant",
|
|
554
|
+
# :content => "Here is your picture of a kitten:",
|
|
555
|
+
# :images => [ "./images/20250804T11303912_resp_abc123/001.png" ],
|
|
556
|
+
# :response => { ... }
|
|
557
|
+
# }
|
|
558
|
+
# ]
|
|
490
559
|
```
|
|
491
560
|
|
|
492
561
|
You can access the image filenames in several ways:
|
|
@@ -497,7 +566,7 @@ images = a.messages.last[:images]
|
|
|
497
566
|
# => ["./images/20250804T11303912_resp_abc123/001.png"]
|
|
498
567
|
|
|
499
568
|
# From the response object
|
|
500
|
-
images = a.messages.last
|
|
569
|
+
images = a.messages.last.dig(:response, :images)
|
|
501
570
|
# => ["./images/20250804T11303912_resp_abc123/001.png"]
|
|
502
571
|
```
|
|
503
572
|
|
|
@@ -508,9 +577,11 @@ a = AI::Chat.new
|
|
|
508
577
|
a.image_generation = true
|
|
509
578
|
a.image_folder = "./images"
|
|
510
579
|
a.user("Draw a picture of a kitten")
|
|
511
|
-
a.generate!
|
|
580
|
+
a.generate!
|
|
581
|
+
# => { :content => "Here is a picture of a kitten:", ... }
|
|
512
582
|
a.user("Make it even cuter")
|
|
513
|
-
a.generate!
|
|
583
|
+
a.generate!
|
|
584
|
+
# => { :content => "Here is the kitten, but even cuter:", ... }
|
|
514
585
|
```
|
|
515
586
|
|
|
516
587
|
## Code Interpreter
|
|
@@ -519,7 +590,23 @@ a.generate! # => { :content => "Here is the kitten, but even cuter:", ... }
|
|
|
519
590
|
y = AI::Chat.new
|
|
520
591
|
y.code_interpreter = true
|
|
521
592
|
y.user("Plot y = 2x*3 when x is -5 to 5.")
|
|
522
|
-
y.generate!
|
|
593
|
+
y.generate!
|
|
594
|
+
# => { :content => "Here is the graph.", ... }
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Background mode
|
|
598
|
+
|
|
599
|
+
If you want to start a response and poll for it later, set `background = true` before calling `generate!`:
|
|
600
|
+
|
|
601
|
+
```ruby
|
|
602
|
+
chat = AI::Chat.new
|
|
603
|
+
chat.background = true
|
|
604
|
+
chat.user("Write a short description about a sci-fi novel about a rat in space.")
|
|
605
|
+
chat.generate!
|
|
606
|
+
|
|
607
|
+
# Poll until it completes (this updates the existing assistant message)
|
|
608
|
+
message = chat.get_response(wait: true, timeout: 600)
|
|
609
|
+
puts message[:content]
|
|
523
610
|
```
|
|
524
611
|
|
|
525
612
|
## Proxying Through prepend.me
|
|
@@ -582,11 +669,11 @@ The `reasoning_effort` parameter guides the model on how many reasoning tokens t
|
|
|
582
669
|
- `"medium"`: Balances speed and reasoning accuracy.
|
|
583
670
|
- `"high"`: Favors more complete reasoning.
|
|
584
671
|
|
|
585
|
-
By default, `reasoning_effort` is `nil`, which means no reasoning parameter is sent to the API. For `gpt-5.
|
|
672
|
+
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.
|
|
586
673
|
|
|
587
674
|
## Advanced: Response Details
|
|
588
675
|
|
|
589
|
-
When you call `generate!` or `
|
|
676
|
+
When you call `generate!` (or later call `get_response` in background mode), the gem stores additional information about the API response:
|
|
590
677
|
|
|
591
678
|
```ruby
|
|
592
679
|
t = AI::Chat.new
|
|
@@ -594,18 +681,18 @@ t.user("Hello!")
|
|
|
594
681
|
t.generate!
|
|
595
682
|
|
|
596
683
|
# Each assistant message includes a response object
|
|
597
|
-
|
|
684
|
+
t.messages.last
|
|
598
685
|
# => {
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
#
|
|
686
|
+
# :role => "assistant",
|
|
687
|
+
# :content => "Hello! How can I help you today?",
|
|
688
|
+
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
|
|
602
689
|
# }
|
|
603
690
|
|
|
604
691
|
# Access detailed information
|
|
605
692
|
response = t.last[:response]
|
|
606
693
|
response[:id] # => "resp_abc123..."
|
|
607
|
-
response[:model] # => "gpt-5.
|
|
608
|
-
response[:usage] # => {:
|
|
694
|
+
response[:model] # => "gpt-5.2"
|
|
695
|
+
response[:usage] # => {:input_tokens=>5, :output_tokens=>7, :total_tokens=>12}
|
|
609
696
|
```
|
|
610
697
|
|
|
611
698
|
This information is useful for:
|
|
@@ -631,7 +718,18 @@ chat.generate!
|
|
|
631
718
|
puts chat.last_response_id # => "resp_xyz789..." (a new ID)
|
|
632
719
|
```
|
|
633
720
|
|
|
634
|
-
This is particularly useful for
|
|
721
|
+
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:
|
|
722
|
+
|
|
723
|
+
```ruby
|
|
724
|
+
require "openai"
|
|
725
|
+
|
|
726
|
+
client = OpenAI::Client.new(api_key: ENV.fetch("OPENAI_API_KEY"))
|
|
727
|
+
|
|
728
|
+
response_id = "resp_abc123..." # e.g., load from your database
|
|
729
|
+
response = client.responses.retrieve(response_id)
|
|
730
|
+
|
|
731
|
+
client.responses.cancel(response_id) unless response.status.to_s == "completed"
|
|
732
|
+
```
|
|
635
733
|
|
|
636
734
|
### Automatic Conversation Management
|
|
637
735
|
|
|
@@ -663,64 +761,117 @@ chat.generate! # Uses the loaded conversation
|
|
|
663
761
|
|
|
664
762
|
## Inspecting Conversation Details
|
|
665
763
|
|
|
666
|
-
The
|
|
667
|
-
|
|
668
|
-
### `items` - Programmatic Access
|
|
669
|
-
|
|
670
|
-
Returns the raw conversation items for programmatic use (displaying in views, filtering, etc.):
|
|
764
|
+
The `get_items` method fetches all conversation items (messages, tool calls, reasoning, etc.) from the API for both programmatic use and debugging:
|
|
671
765
|
|
|
672
766
|
```ruby
|
|
673
767
|
chat = AI::Chat.new
|
|
768
|
+
chat.reasoning_effort = "high" # Enable reasoning summaries
|
|
674
769
|
chat.web_search = true
|
|
675
770
|
chat.user("Search for Ruby tutorials")
|
|
676
771
|
chat.generate!
|
|
677
772
|
|
|
678
773
|
# Get all conversation items (chronological order by default)
|
|
679
|
-
|
|
774
|
+
chat.get_items
|
|
680
775
|
|
|
681
|
-
#
|
|
682
|
-
|
|
776
|
+
# Output in IRB/Rails console:
|
|
777
|
+
# ┌────────────────────────────────────────────────────────────────────────────┐
|
|
778
|
+
# │ Conversation: conv_6903c1eea6cc819695af3a1b1ebf9b390c3db5e8ec021c9a │
|
|
779
|
+
# │ Items: 8 │
|
|
780
|
+
# └────────────────────────────────────────────────────────────────────────────┘
|
|
781
|
+
#
|
|
782
|
+
# [detailed colorized output of all items including web searches,
|
|
783
|
+
# reasoning summaries, tool calls, messages, etc.]
|
|
784
|
+
|
|
785
|
+
# Iterate over items programmatically
|
|
786
|
+
chat.get_items.data.each do |item|
|
|
683
787
|
case item.type
|
|
684
788
|
when :message
|
|
685
789
|
puts "#{item.role}: #{item.content.first.text}"
|
|
686
790
|
when :web_search_call
|
|
687
|
-
puts "Web search: #{item.action.query}"
|
|
688
|
-
puts "Results: #{item.results.length}"
|
|
791
|
+
puts "Web search: #{item.action.query}" if item.action.respond_to?(:query) && item.action.query
|
|
689
792
|
when :reasoning
|
|
690
|
-
|
|
793
|
+
# Reasoning summaries show a high-level view of the model's reasoning
|
|
794
|
+
if item.summary&.first
|
|
795
|
+
puts "Reasoning: #{item.summary.first.text}"
|
|
796
|
+
end
|
|
797
|
+
when :image_generation_call
|
|
798
|
+
puts "Image generated" if item.result
|
|
691
799
|
end
|
|
692
800
|
end
|
|
693
801
|
|
|
694
802
|
# For long conversations, you can request reverse chronological order
|
|
695
803
|
# (useful for pagination to get most recent items first)
|
|
696
|
-
recent_items = chat.
|
|
804
|
+
recent_items = chat.get_items(order: :desc)
|
|
697
805
|
```
|
|
698
806
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
Pretty-prints the entire conversation with all details for debugging and learning:
|
|
702
|
-
|
|
703
|
-
```ruby
|
|
704
|
-
chat.verbose
|
|
705
|
-
|
|
706
|
-
# Output:
|
|
707
|
-
# ┌────────────────────────────────────────────────────────────────────────────┐
|
|
708
|
-
# │ Conversation: conv_6903c1eea6cc819695af3a1b1ebf9b390c3db5e8ec021c9a │
|
|
709
|
-
# │ Items: 3 │
|
|
710
|
-
# └────────────────────────────────────────────────────────────────────────────┘
|
|
711
|
-
#
|
|
712
|
-
# [detailed colorized output of all items including web searches,
|
|
713
|
-
# reasoning, tool calls, messages, etc.]
|
|
714
|
-
```
|
|
807
|
+
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.
|
|
715
808
|
|
|
716
809
|
This is useful for:
|
|
717
810
|
- **Learning** how the model uses tools (web search, code interpreter, etc.)
|
|
718
811
|
- **Debugging** why the model made certain decisions
|
|
719
812
|
- **Understanding** the full context beyond just the final response
|
|
813
|
+
- **Transparency** into the model's reasoning process
|
|
814
|
+
|
|
815
|
+
### HTML Output for ERB Templates
|
|
816
|
+
|
|
817
|
+
All display objects have a `to_html` method for rendering in ERB templates:
|
|
818
|
+
|
|
819
|
+
```erb
|
|
820
|
+
<%# Display a chat object %>
|
|
821
|
+
<%= @chat.to_html %>
|
|
822
|
+
|
|
823
|
+
<%# Display individual messages %>
|
|
824
|
+
<% @chat.messages.each do |msg| %>
|
|
825
|
+
<%= msg.to_html %>
|
|
826
|
+
<% end %>
|
|
827
|
+
|
|
828
|
+
<%# Display conversation items (quick debug view) %>
|
|
829
|
+
<%= @chat.get_items.to_html %>
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
The HTML output includes a dark background to match the terminal aesthetic.
|
|
833
|
+
|
|
834
|
+
You can also loop over `get_items.data` to build custom displays showing reasoning steps, tool calls, etc.:
|
|
835
|
+
|
|
836
|
+
```erb
|
|
837
|
+
<% @chat.get_items.data.each do |item| %>
|
|
838
|
+
<% case item.type.to_s %>
|
|
839
|
+
<% when "message" %>
|
|
840
|
+
<div class="message <%= item.role %>">
|
|
841
|
+
<strong><%= item.role.capitalize %>:</strong>
|
|
842
|
+
<% if item.content&.first %>
|
|
843
|
+
<% content = item.content.first %>
|
|
844
|
+
<% if content.type.to_s == "input_text" %>
|
|
845
|
+
<%= content.text %>
|
|
846
|
+
<% elsif content.type.to_s == "output_text" %>
|
|
847
|
+
<%= content.text %>
|
|
848
|
+
<% end %>
|
|
849
|
+
<% end %>
|
|
850
|
+
</div>
|
|
851
|
+
<% when "reasoning" %>
|
|
852
|
+
<% if item.summary&.first %>
|
|
853
|
+
<details class="reasoning">
|
|
854
|
+
<summary>Reasoning</summary>
|
|
855
|
+
<%= item.summary.first.text %>
|
|
856
|
+
</details>
|
|
857
|
+
<% end %>
|
|
858
|
+
<% when "web_search_call" %>
|
|
859
|
+
<% if item.action.respond_to?(:query) && item.action.query %>
|
|
860
|
+
<div class="web-search">
|
|
861
|
+
Searched: "<%= item.action.query %>"
|
|
862
|
+
</div>
|
|
863
|
+
<% end %>
|
|
864
|
+
<% when "image_generation_call" %>
|
|
865
|
+
<div class="image-generation">
|
|
866
|
+
Image generated
|
|
867
|
+
</div>
|
|
868
|
+
<% end %>
|
|
869
|
+
<% end %>
|
|
870
|
+
```
|
|
720
871
|
|
|
721
872
|
## Setting messages directly
|
|
722
873
|
|
|
723
|
-
You can use `.messages=()` to assign an `Array` of `Hashes
|
|
874
|
+
You can use `.messages=()` to assign an `Array` of `Hashes` (text-only). Each `Hash` must have keys `:role` and `:content`:
|
|
724
875
|
|
|
725
876
|
```ruby
|
|
726
877
|
# Using the planet example with array of hashes
|
|
@@ -743,80 +894,8 @@ response = p.generate!
|
|
|
743
894
|
puts response
|
|
744
895
|
```
|
|
745
896
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
```ruby
|
|
749
|
-
# Create a new chat instance
|
|
750
|
-
q = AI::Chat.new
|
|
751
|
-
|
|
752
|
-
# With images
|
|
753
|
-
q.messages = [
|
|
754
|
-
{ role: "system", content: "You are a helpful assistant." },
|
|
755
|
-
{ role: "user", content: "What's in this image?", image: "path/to/image.jpg" },
|
|
756
|
-
]
|
|
757
|
-
|
|
758
|
-
# With multiple images
|
|
759
|
-
q.messages = [
|
|
760
|
-
{ role: "system", content: "You are a helpful assistant." },
|
|
761
|
-
{ role: "user", content: "Compare these images", images: ["image1.jpg", "image2.jpg"] }
|
|
762
|
-
]
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
## Other Features Being Considered
|
|
766
|
-
|
|
767
|
-
- **Streaming responses**: Real-time streaming as the AI generates its response
|
|
768
|
-
- **Cost tracking**: Automatic calculation and tracking of API costs
|
|
769
|
-
- **Token usage helpers**: Convenience methods like `total_tokens` to sum usage across all responses in a conversation
|
|
770
|
-
|
|
771
|
-
## TODO: Missing Test Coverage
|
|
772
|
-
|
|
773
|
-
The following gem-specific logic would benefit from additional RSpec test coverage:
|
|
774
|
-
|
|
775
|
-
1. **Schema format normalization** - The `wrap_schema_if_needed` method detects and wraps 3 different input formats (raw, named, already-wrapped). This complex conditional logic could silently regress.
|
|
776
|
-
|
|
777
|
-
2. **Multimodal content array building** - The `add` method builds nested structures when images/files are provided, handling `image`/`images` and `file`/`files` parameters with specific ordering (text → images → files).
|
|
778
|
-
|
|
779
|
-
3. **File classification and processing** - `classify_obj` and `process_file_input` distinguish URLs vs file paths vs file-like objects, with MIME type detection determining encoding behavior.
|
|
780
|
-
|
|
781
|
-
4. **Message preparation after response** - `prepare_messages_for_api` has slicing logic that only sends messages after the last response, preventing re-sending entire conversation history.
|
|
782
|
-
|
|
783
|
-
These are all gem-specific transformations (not just OpenAI pass-through) that could regress without proper test coverage.
|
|
784
|
-
|
|
785
|
-
## TODO: Code Quality
|
|
786
|
-
|
|
787
|
-
Address Reek warnings (`bundle exec reek`). Currently 29 warnings for code smells like:
|
|
788
|
-
|
|
789
|
-
- `TooManyStatements` in several methods
|
|
790
|
-
- `DuplicateMethodCall` in `extract_and_save_files`, `verbose`, etc.
|
|
791
|
-
- `RepeatedConditional` for `proxy` checks
|
|
792
|
-
- `FeatureEnvy` in `parse_response` and `wait_for_response`
|
|
793
|
-
|
|
794
|
-
These don't affect functionality but indicate areas for refactoring.
|
|
795
|
-
|
|
796
|
-
Then, add `quality` back as a CI check.
|
|
797
|
-
|
|
798
|
-
## Testing with Real API Calls
|
|
799
|
-
|
|
800
|
-
While this gem includes specs, they use mocked API responses. To test with real API calls:
|
|
801
|
-
|
|
802
|
-
1. Create a `.env` file at the project root with your API credentials:
|
|
803
|
-
```
|
|
804
|
-
# Your OpenAI API key
|
|
805
|
-
OPENAI_API_KEY=your_openai_api_key_here
|
|
806
|
-
```
|
|
807
|
-
2. Install dependencies: `bundle install`
|
|
808
|
-
3. Run the examples: `bundle exec ruby examples/all.rb`
|
|
809
|
-
|
|
810
|
-
This test program runs through all the major features of the gem, making real API calls to OpenAI.
|
|
897
|
+
For images/files, prefer using `chat.user(..., image:/images:/file:/files:)` so the gem can build the correct multimodal structure.
|
|
811
898
|
|
|
812
899
|
## Contributing
|
|
813
900
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
1. **Code Style**: This project uses StandardRB for linting. Run `bundle exec standardrb --fix` before committing to automatically fix style issues.
|
|
817
|
-
|
|
818
|
-
2. **Testing**: Ensure all specs pass with `bundle exec rspec`.
|
|
819
|
-
|
|
820
|
-
3. **Examples**: If adding a feature, consider adding an example in the `examples/` directory.
|
|
821
|
-
|
|
822
|
-
4. **Documentation**: Update the README if your changes affect the public API.
|
|
901
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
data/ai-chat.gemspec
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
4
|
spec.name = "ai-chat"
|
|
5
|
-
spec.version = "0.
|
|
6
|
-
spec.authors = ["Raghu Betina"]
|
|
7
|
-
spec.email = ["raghu@firstdraft.com"]
|
|
5
|
+
spec.version = "0.5.0"
|
|
6
|
+
spec.authors = ["Raghu Betina", "Jelani Woods"]
|
|
7
|
+
spec.email = ["raghu@firstdraft.com", "jelani@firstdraft.com"]
|
|
8
8
|
spec.homepage = "https://github.com/firstdraft/ai-chat"
|
|
9
9
|
spec.summary = "A beginner-friendly Ruby interface for OpenAI's API"
|
|
10
10
|
spec.license = "MIT"
|
data/lib/ai/amazing_print.rb
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
require "amazing_print"
|
|
2
|
+
|
|
3
|
+
# Fix AmazingPrint's colorless method to strip HTML tags in addition to ANSI codes.
|
|
4
|
+
# Without this, alignment is broken when html: true because colorless_size
|
|
5
|
+
# doesn't account for <kbd> tag lengths.
|
|
6
|
+
# TODO: Remove if https://github.com/amazing-print/amazing_print/pull/146 is merged.
|
|
7
|
+
module AmazingPrint
|
|
8
|
+
module Formatters
|
|
9
|
+
class BaseFormatter
|
|
10
|
+
alias_method :original_colorless, :colorless
|
|
11
|
+
|
|
12
|
+
def colorless(string)
|
|
13
|
+
result = original_colorless(string)
|
|
14
|
+
result.gsub(/<kbd[^>]*>|<\/kbd>/, "")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
2
20
|
# :reek:IrresponsibleModule
|
|
3
21
|
module AmazingPrint
|
|
4
22
|
module AI
|
|
@@ -27,33 +45,11 @@ module AmazingPrint
|
|
|
27
45
|
end
|
|
28
46
|
end
|
|
29
47
|
|
|
30
|
-
# :reek:DuplicateMethodCall
|
|
31
48
|
# :reek:FeatureEnvy
|
|
32
|
-
# :reek:NilCheck
|
|
33
|
-
# :reek:TooManyStatements
|
|
34
49
|
def format_ai_chat(chat)
|
|
35
|
-
vars =
|
|
36
|
-
|
|
37
|
-
# Format messages with truncation
|
|
38
|
-
if chat.instance_variable_defined?(:@messages)
|
|
39
|
-
messages = chat.instance_variable_get(:@messages).map do |msg|
|
|
40
|
-
truncated_msg = msg.dup
|
|
41
|
-
if msg[:content].is_a?(String) && msg[:content].length > 80
|
|
42
|
-
truncated_msg[:content] = msg[:content][0..77] + "..."
|
|
43
|
-
end
|
|
44
|
-
truncated_msg
|
|
45
|
-
end
|
|
46
|
-
vars << ["@messages", messages]
|
|
50
|
+
vars = chat.inspectable_attributes.map do |(name, value)|
|
|
51
|
+
[name.to_s, value]
|
|
47
52
|
end
|
|
48
|
-
|
|
49
|
-
# Add other variables (except sensitive ones)
|
|
50
|
-
skip_vars = [:@api_key, :@client, :@messages]
|
|
51
|
-
chat.instance_variables.sort.each do |var|
|
|
52
|
-
next if skip_vars.include?(var)
|
|
53
|
-
value = chat.instance_variable_get(var)
|
|
54
|
-
vars << [var.to_s, value] unless value.nil?
|
|
55
|
-
end
|
|
56
|
-
|
|
57
53
|
format_object(chat, vars)
|
|
58
54
|
end
|
|
59
55
|
|
|
@@ -65,10 +61,13 @@ module AmazingPrint
|
|
|
65
61
|
"#{name}: #{inspector.awesome(value)}"
|
|
66
62
|
end
|
|
67
63
|
|
|
64
|
+
lt = @options[:html] ? "<" : "<"
|
|
65
|
+
gt = @options[:html] ? ">" : ">"
|
|
66
|
+
|
|
68
67
|
if @options[:multiline]
|
|
69
|
-
"
|
|
68
|
+
"##{lt}#{object.class}\n#{data.map { |line| " #{line}" }.join("\n")}\n#{gt}"
|
|
70
69
|
else
|
|
71
|
-
"
|
|
70
|
+
"##{lt}#{object.class} #{data.join(", ")}#{gt}"
|
|
72
71
|
end
|
|
73
72
|
end
|
|
74
73
|
end
|
data/lib/ai/chat.rb
CHANGED
|
@@ -12,7 +12,6 @@ require "tty-spinner"
|
|
|
12
12
|
require "timeout"
|
|
13
13
|
|
|
14
14
|
require_relative "http"
|
|
15
|
-
include AI::Http
|
|
16
15
|
|
|
17
16
|
module AI
|
|
18
17
|
# :reek:MissingSafeMethod { exclude: [ generate! ] }
|
|
@@ -21,6 +20,8 @@ module AI
|
|
|
21
20
|
# :reek:InstanceVariableAssumption
|
|
22
21
|
# :reek:IrresponsibleModule
|
|
23
22
|
class Chat
|
|
23
|
+
include AI::Http
|
|
24
|
+
|
|
24
25
|
# :reek:Attribute
|
|
25
26
|
attr_accessor :background, :code_interpreter, :conversation_id, :image_generation, :image_folder, :messages, :model, :proxy, :reasoning_effort, :web_search
|
|
26
27
|
attr_reader :client, :last_response_id, :schema, :schema_file
|
|
@@ -31,7 +32,7 @@ module AI
|
|
|
31
32
|
@api_key = api_key || ENV.fetch(api_key_env_var)
|
|
32
33
|
@messages = []
|
|
33
34
|
@reasoning_effort = nil
|
|
34
|
-
@model = "gpt-5.
|
|
35
|
+
@model = "gpt-5.2"
|
|
35
36
|
@client = OpenAI::Client.new(api_key: @api_key)
|
|
36
37
|
@last_response_id = nil
|
|
37
38
|
@proxy = false
|
|
@@ -47,7 +48,7 @@ module AI
|
|
|
47
48
|
json = if proxy
|
|
48
49
|
uri = URI(PROXY_URL + "api.openai.com/v1/responses")
|
|
49
50
|
parameters = {
|
|
50
|
-
model: "gpt-5.
|
|
51
|
+
model: "gpt-5.2",
|
|
51
52
|
input: [
|
|
52
53
|
{role: :system, content: system_prompt},
|
|
53
54
|
{role: :user, content: description}
|
|
@@ -60,7 +61,7 @@ module AI
|
|
|
60
61
|
else
|
|
61
62
|
client = OpenAI::Client.new(api_key: api_key)
|
|
62
63
|
response = client.responses.create(
|
|
63
|
-
model: "gpt-5.
|
|
64
|
+
model: "gpt-5.2",
|
|
64
65
|
input: [
|
|
65
66
|
{role: :system, content: system_prompt},
|
|
66
67
|
{role: :user, content: description}
|
|
@@ -84,15 +85,12 @@ module AI
|
|
|
84
85
|
# :reek:TooManyStatements
|
|
85
86
|
# :reek:NilCheck
|
|
86
87
|
def add(content, role: "user", response: nil, status: nil, image: nil, images: nil, file: nil, files: nil)
|
|
87
|
-
if image.nil? && images.nil? && file.nil? && files.nil?
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
message[:content] = content if content
|
|
94
|
-
message[:status] = status if status
|
|
95
|
-
messages.push(message)
|
|
88
|
+
message = if image.nil? && images.nil? && file.nil? && files.nil?
|
|
89
|
+
msg = Message[role: role]
|
|
90
|
+
msg[:content] = content if content
|
|
91
|
+
msg[:response] = response if response
|
|
92
|
+
msg[:status] = status if status
|
|
93
|
+
msg
|
|
96
94
|
else
|
|
97
95
|
text_and_files_array = [
|
|
98
96
|
{
|
|
@@ -122,14 +120,15 @@ module AI
|
|
|
122
120
|
text_and_files_array.push(process_file_input(file))
|
|
123
121
|
end
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
)
|
|
123
|
+
Message[
|
|
124
|
+
role: role,
|
|
125
|
+
content: text_and_files_array,
|
|
126
|
+
status: status
|
|
127
|
+
]
|
|
132
128
|
end
|
|
129
|
+
|
|
130
|
+
messages.push(message)
|
|
131
|
+
message
|
|
133
132
|
end
|
|
134
133
|
|
|
135
134
|
def system(message)
|
|
@@ -189,10 +188,10 @@ module AI
|
|
|
189
188
|
messages.last
|
|
190
189
|
end
|
|
191
190
|
|
|
192
|
-
def
|
|
191
|
+
def get_items(order: :asc)
|
|
193
192
|
raise "No conversation_id set. Call generate! first to create a conversation." unless conversation_id
|
|
194
193
|
|
|
195
|
-
if proxy
|
|
194
|
+
raw_items = if proxy
|
|
196
195
|
uri = URI(PROXY_URL + "api.openai.com/v1/conversations/#{conversation_id}/items?order=#{order}")
|
|
197
196
|
response_hash = send_request(uri, content_type: "json", method: "get")
|
|
198
197
|
|
|
@@ -215,62 +214,50 @@ module AI
|
|
|
215
214
|
else
|
|
216
215
|
client.conversations.items.list(conversation_id, order: order)
|
|
217
216
|
end
|
|
217
|
+
|
|
218
|
+
Items.new(raw_items, conversation_id: conversation_id)
|
|
218
219
|
end
|
|
219
220
|
|
|
220
|
-
def
|
|
221
|
-
|
|
221
|
+
def inspectable_attributes
|
|
222
|
+
attrs = []
|
|
223
|
+
|
|
224
|
+
# 1. Model and reasoning (configuration)
|
|
225
|
+
attrs << [:@model, @model]
|
|
226
|
+
attrs << [:@reasoning_effort, @reasoning_effort]
|
|
227
|
+
|
|
228
|
+
# 2. Conversation state
|
|
229
|
+
attrs << [:@conversation_id, @conversation_id]
|
|
230
|
+
attrs << [:@last_response_id, @last_response_id] if @last_response_id
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
# 3. Messages (the main content, without response details)
|
|
233
|
+
display_messages = @messages.map { |msg| msg.except(:response) }
|
|
234
|
+
attrs << [:@messages, display_messages]
|
|
225
235
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
puts "└#{"─" * (box_width - 2)}┘"
|
|
231
|
-
puts
|
|
236
|
+
# 4. Optional features (only if enabled/changed from default)
|
|
237
|
+
attrs << [:@proxy, @proxy] if @proxy != false
|
|
238
|
+
attrs << [:@image_generation, @image_generation] if @image_generation != false
|
|
239
|
+
attrs << [:@image_folder, @image_folder] if @image_folder != "./images"
|
|
232
240
|
|
|
233
|
-
|
|
241
|
+
# 5. Optional state (only if set)
|
|
242
|
+
attrs << [:@background, @background] if @background
|
|
243
|
+
attrs << [:@code_interpreter, @code_interpreter] if @code_interpreter
|
|
244
|
+
attrs << [:@web_search, @web_search] if @web_search
|
|
245
|
+
attrs << [:@schema, @schema] if @schema
|
|
246
|
+
attrs << [:@schema_file, @schema_file] if @schema_file
|
|
247
|
+
|
|
248
|
+
attrs
|
|
234
249
|
end
|
|
235
250
|
|
|
236
251
|
def inspect
|
|
237
|
-
|
|
252
|
+
ai(plain: !$stdout.tty?, multiline: true)
|
|
238
253
|
end
|
|
239
254
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def pretty_print(q)
|
|
247
|
-
q.group(1, "#<#{self.class}", ">") do
|
|
248
|
-
q.breakable
|
|
249
|
-
|
|
250
|
-
# Show messages with truncation
|
|
251
|
-
q.text "@messages="
|
|
252
|
-
truncated_messages = @messages.map do |msg|
|
|
253
|
-
truncated_msg = msg.dup
|
|
254
|
-
if msg[:content].is_a?(String) && msg[:content].length > 80
|
|
255
|
-
truncated_msg[:content] = msg[:content][0..77] + "..."
|
|
256
|
-
end
|
|
257
|
-
truncated_msg
|
|
258
|
-
end
|
|
259
|
-
q.pp truncated_messages
|
|
260
|
-
|
|
261
|
-
# Show other instance variables (except sensitive ones)
|
|
262
|
-
skip_vars = [:@messages, :@api_key, :@client]
|
|
263
|
-
instance_variables.sort.each do |var|
|
|
264
|
-
next if skip_vars.include?(var)
|
|
265
|
-
value = instance_variable_get(var)
|
|
266
|
-
unless value.nil?
|
|
267
|
-
q.text ","
|
|
268
|
-
q.breakable
|
|
269
|
-
q.text "#{var}="
|
|
270
|
-
q.pp value
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
255
|
+
def to_html
|
|
256
|
+
AI.wrap_html(ai(html: true, multiline: true))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def pretty_inspect
|
|
260
|
+
"#{inspect}\n"
|
|
274
261
|
end
|
|
275
262
|
|
|
276
263
|
private
|
|
@@ -312,7 +299,7 @@ module AI
|
|
|
312
299
|
parameters[:background] = background if background
|
|
313
300
|
parameters[:tools] = tools unless tools.empty?
|
|
314
301
|
parameters[:text] = schema if schema
|
|
315
|
-
parameters[:reasoning] = {effort: reasoning_effort} if reasoning_effort
|
|
302
|
+
parameters[:reasoning] = {effort: reasoning_effort, summary: "auto"} if reasoning_effort
|
|
316
303
|
|
|
317
304
|
create_conversation unless conversation_id
|
|
318
305
|
parameters[:conversation] = conversation_id
|
|
@@ -387,12 +374,12 @@ module AI
|
|
|
387
374
|
message.dig(:response, :id) == response_id
|
|
388
375
|
end
|
|
389
376
|
|
|
390
|
-
message =
|
|
377
|
+
message = Message[
|
|
391
378
|
role: "assistant",
|
|
392
379
|
content: response_content,
|
|
393
380
|
response: chat_response,
|
|
394
381
|
status: response_status
|
|
395
|
-
|
|
382
|
+
]
|
|
396
383
|
|
|
397
384
|
message.store(:images, image_filenames) unless image_filenames.empty?
|
|
398
385
|
|
|
@@ -400,8 +387,9 @@ module AI
|
|
|
400
387
|
messages[existing_message_position] = message
|
|
401
388
|
else
|
|
402
389
|
messages.push(message)
|
|
403
|
-
message
|
|
404
390
|
end
|
|
391
|
+
|
|
392
|
+
message
|
|
405
393
|
end
|
|
406
394
|
|
|
407
395
|
def cancel_request
|
data/lib/ai/items.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module AI
|
|
6
|
+
class Items < SimpleDelegator
|
|
7
|
+
def initialize(response, conversation_id:)
|
|
8
|
+
super(response)
|
|
9
|
+
@conversation_id = conversation_id
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_html
|
|
13
|
+
AI.wrap_html(build_output(html: true))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def inspect
|
|
17
|
+
build_output(html: false, plain: !$stdout.tty?)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pretty_inspect
|
|
21
|
+
"#{inspect}\n"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pretty_print(q)
|
|
25
|
+
q.output << inspect
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_output(html: false, plain: false)
|
|
31
|
+
box = build_box
|
|
32
|
+
items_output = data.ai(html: html, plain: plain, limit: 100, indent: 2, index: true)
|
|
33
|
+
|
|
34
|
+
if html
|
|
35
|
+
"<pre>#{box}</pre>\n#{items_output}"
|
|
36
|
+
else
|
|
37
|
+
"#{box}\n#{items_output}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_box
|
|
42
|
+
box_width = 78
|
|
43
|
+
inner_width = box_width - 4
|
|
44
|
+
|
|
45
|
+
lines = []
|
|
46
|
+
lines << "┌#{"─" * (box_width - 2)}┐"
|
|
47
|
+
lines << "│ Conversation: #{@conversation_id.to_s.ljust(inner_width - 14)} │"
|
|
48
|
+
lines << "│ Items: #{data.length.to_s.ljust(inner_width - 7)} │"
|
|
49
|
+
lines << "└#{"─" * (box_width - 2)}┘"
|
|
50
|
+
|
|
51
|
+
lines.join("\n")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/ai/message.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AI
|
|
4
|
+
class Message < Hash
|
|
5
|
+
def inspect
|
|
6
|
+
ai(plain: !$stdout.tty?, index: false)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def pretty_inspect
|
|
10
|
+
"#{inspect}\n"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# IRB's ColorPrinter calls pretty_print and re-colorizes text,
|
|
14
|
+
# which escapes our ANSI codes. Write directly to output to bypass.
|
|
15
|
+
def pretty_print(q)
|
|
16
|
+
q.output << inspect
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_html
|
|
20
|
+
AI.wrap_html(ai(html: true, index: false))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/ai-chat.rb
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
module AI
|
|
2
|
+
HTML_PRE_STYLE = "background-color: #1e1e1e; color: #d4d4d4; padding: 1em; white-space: pre-wrap; word-wrap: break-word;"
|
|
3
|
+
|
|
4
|
+
def self.wrap_html(html)
|
|
5
|
+
html = html.gsub("<pre>", "<pre style=\"#{HTML_PRE_STYLE}\">")
|
|
6
|
+
html.respond_to?(:html_safe) ? html.html_safe : html
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative "ai/message"
|
|
11
|
+
require_relative "ai/items"
|
|
1
12
|
require_relative "ai/chat"
|
|
2
13
|
|
|
3
14
|
# Load amazing_print extension if amazing_print is available
|
metadata
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ai-chat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raghu Betina
|
|
8
|
+
- Jelani Woods
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
@@ -143,6 +144,7 @@ dependencies:
|
|
|
143
144
|
version: '11.1'
|
|
144
145
|
email:
|
|
145
146
|
- raghu@firstdraft.com
|
|
147
|
+
- jelani@firstdraft.com
|
|
146
148
|
executables: []
|
|
147
149
|
extensions: []
|
|
148
150
|
extra_rdoc_files:
|
|
@@ -156,6 +158,8 @@ files:
|
|
|
156
158
|
- lib/ai/amazing_print.rb
|
|
157
159
|
- lib/ai/chat.rb
|
|
158
160
|
- lib/ai/http.rb
|
|
161
|
+
- lib/ai/items.rb
|
|
162
|
+
- lib/ai/message.rb
|
|
159
163
|
- lib/prompts/schema_generator.md
|
|
160
164
|
homepage: https://github.com/firstdraft/ai-chat
|
|
161
165
|
licenses:
|
|
@@ -181,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
181
185
|
- !ruby/object:Gem::Version
|
|
182
186
|
version: '0'
|
|
183
187
|
requirements: []
|
|
184
|
-
rubygems_version:
|
|
188
|
+
rubygems_version: 4.0.1
|
|
185
189
|
specification_version: 4
|
|
186
190
|
summary: A beginner-friendly Ruby interface for OpenAI's API
|
|
187
191
|
test_files: []
|