ai-chat 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 975e7f80044ac46d72ad1c08e290d7fa71b43048fe76d68784d3961c36efde95
4
- data.tar.gz: cf0a2b5fcee3e6ee413580419c992efe6c50e5935be875f2e82d8a740c7d15fb
3
+ metadata.gz: 77efcbc43e3402b184ce49ff870671796904a0935a37d55dd9eeaaea93572b53
4
+ data.tar.gz: 8996a14db31e7455815d10acac9827658fcbaf5358e1a461d2b83a946b8f3cc9
5
5
  SHA512:
6
- metadata.gz: fe727e64a0388922db85c3085dea85c6622b88c886cbb25fc660dc9bad8291205a1904a19f611457ac83cbe673a299db4d97f0b658e236de0e3ccccb46f44aac
7
- data.tar.gz: 4be9d9a80ea39e20ef2c11d8142f9403b7b43f46c8f4ccc5a80f88a027980be11b1e882165e67e0380d2a8982e0e4bbd3489585cac3c4a0a291d46a75813d53d
6
+ metadata.gz: b1993b6e812a75387b53ade7f29785a80aa0c70c7b6aa97d5ac9eed7e73fc27a87f1a76676e9878416849af3c9ae3967d6b78838be0c681f077fa63c81e32993
7
+ data.tar.gz: 0f463e8beef43965db7b85b94335624322f46bc39cc4dd2c1a02ad44993ae972bdbfb4a670890b3f55ddf9ce5ebf57fd8b2e92f85fd3340a2be1d7aa6d1ca549
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
- pp a.messages
87
- # => [{:role=>"user", :content=>"If the Ruby community had an official motto, what might it be?"}]
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! # => { :role => "assistant", :content => "Matz is nice and so we are nice" (or similar) }
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
- pp a.messages
104
+ a.messages
94
105
  # => [
95
- # {:role=>"user", :content=>"If the Ruby community had an official motto, what might it be?"},
96
- # {:role=>"assistant", :content=>"Matz is nice and so we are nice", :response => { id=resp_abc... model=gpt-5.1 tokens=12 } }
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! # => { :role => "assistant", :content => "Convention over configuration."}
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
- {:role => "system", :content => "You are a helpful assistant"},
115
- {:role => "user", :content => "Hello!"},
116
- {:role => "assistant", :content => "Hi there! How can I help you today?", :response => { id=resp_abc... model=gpt-5.1 tokens=12 } }
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
- pp b.messages
169
+ b.messages
137
170
  # => [
138
- # {:role=>"system", :content=>"You are a helpful assistant that talks like Shakespeare."},
139
- # {:role=>"user", :content=>"If the Ruby community had an official motto, what might it be?"}
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! # => { :role => "assistant", :content => "Methinks 'tis 'Ruby doth bring joy to all who craft with care'" }
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.1` model. If you want to use a different model, you can set it:
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
- pp h.messages
266
+ h.messages
223
267
  # => [
224
- # {:role=>"system", :content=>"You are a helpful cooking assistant"},
225
- # {:role=>"user", :content=>"How do I boil an egg?"},
226
- # {:role=>"assistant", :content=>"Here's how to boil an egg..."}
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! # => { :content => "Here is your picture of a kitten:", ... }
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! # => { :content => "Here is your picture of a kitten:", ... }
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
- pp a.messages
546
+ a.messages
486
547
  # => [
487
- # {:role=>"user", :content=>"Draw a picture of a kitten"},
488
- # {:role=>"assistant", :content=>"Here is your picture of a kitten:", :images => ["./images/20250804T11303912_resp_abc123/001.png"], :response => #<Response ...>}
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[:response].images
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! # => { :content => "Here is a picture of a kitten:", ... }
580
+ a.generate!
581
+ # => { :content => "Here is a picture of a kitten:", ... }
512
582
  a.user("Make it even cuter")
513
- a.generate! # => { :content => "Here is the kitten, but even cuter:", ... }
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! # => {:content => "Here is the graph.", ... }
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.1` (the default model), this is equivalent to `"none"` reasoning.
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 `generate!`, the gem stores additional information about the API response:
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
- pp t.messages.last
684
+ t.messages.last
598
685
  # => {
599
- # :role => "assistant",
600
- # :content => "Hello! How can I help you today?",
601
- # :response => { id=resp_abc... model=gpt-5.1 tokens=12 }
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.1"
608
- response[:usage] # => {:prompt_tokens=>5, :completion_tokens=>7, :total_tokens=>12}
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 managing background tasks. When you make a request in background mode, you can immediately get the `last_response_id` to track, retrieve, or cancel that specific job later from a different process.
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 gem provides two methods to inspect what happened during a conversation:
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
- page = chat.items
774
+ chat.get_items
680
775
 
681
- # Access item data
682
- page.data.each do |item|
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
- puts "Reasoning: #{item.summary.first.text}"
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.items(order: :desc)
804
+ recent_items = chat.get_items(order: :desc)
697
805
  ```
698
806
 
699
- ### `verbose` - Terminal Output
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`. Each `Hash` must have keys `:role` and `:content`, and optionally `:image` or `:images`:
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
- You can still include images:
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
- When contributing to this project:
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.4.0"
6
- spec.authors = ["Raghu Betina"]
7
- spec.email = ["raghu@firstdraft.com"]
5
+ spec.version = "0.5.1"
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"
@@ -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] ? "&lt;" : "<"
65
+ gt = @options[:html] ? "&gt;" : ">"
66
+
68
67
  if @options[:multiline]
69
- "#<#{object.class}\n#{data.map { |line| " #{line}" }.join("\n")}\n>"
68
+ "##{lt}#{object.class}\n#{data.map { |line| " #{line}" }.join("\n")}\n#{gt}"
70
69
  else
71
- "#<#{object.class} #{data.join(", ")}>"
70
+ "##{lt}#{object.class} #{data.join(", ")}#{gt}"
72
71
  end
73
72
  end
74
73
  end