From 6f9388fc06bae7e4fab8af3c43995b0a15c1ae8a Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 30 Jul 2025 13:04:21 -0500 Subject: [PATCH 1/6] Add custom amazing_print formatting for AI::Chat Implement to_hash method that provides a clean, readable format when using amazing_print. Features include: - Dynamic inclusion of all instance variables - Security: excludes @api_key and @client - Truncates long message content to 80 chars - Skips nil values for cleaner output --- Gemfile.lock | 6 ++++-- lib/ai/chat.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a959531..3f63b76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,13 @@ PATH remote: . specs: - ai-chat (0.1.0) + ai-chat (0.1.1) base64 (~> 0.1) mime-types (~> 3.0) openai (~> 0.14) - refinements (~> 11.1) zeitwerk (~> 2.7) + openai (0.16.0) + connection_pool GEM remote: https://rubygems.org/ @@ -169,6 +170,7 @@ DEPENDENCIES dotenv rake (~> 13.3) reek (~> 6.5) + refinements (~> 11.1) repl_type_completor (~> 0.1) rspec (~> 3.13) simplecov (~> 0.22) diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 0e244b9..1f0b603 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -181,6 +181,35 @@ def inspect "#<#{self.class.name} @messages=#{messages.inspect} @model=#{@model.inspect} @schema=#{@schema.inspect} @reasoning_effort=#{@reasoning_effort.inspect}>" end + def to_hash + hash = { "#{self.class.name}" => {} } + + # Define which instance variables to skip + skip_vars = [:@api_key, :@client] + + instance_variables.sort.each do |var| + next if skip_vars.include?(var) + + value = instance_variable_get(var) + + # Special handling for @messages to truncate content + if var == :@messages + value = value.map do |msg| + truncated_msg = msg.dup + if msg[:content].is_a?(String) && msg[:content].length > 80 + truncated_msg[:content] = msg[:content][0..77] + "..." + end + truncated_msg + end + end + + # Skip nil values for cleaner output + hash["#{self.class.name}"][var.to_s] = value unless value.nil? + end + + hash + end + private # Custom exception class for input classification errors. From e5416932024c698a3ce9f33f0055454a52681947 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 30 Jul 2025 13:19:33 -0500 Subject: [PATCH 2/6] Add amazing_print support for AI::Response Implement to_hash method that dynamically includes all instance variables for clean formatting. This makes Response objects display with proper indentation when nested within Chat objects. --- lib/ai/response.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/ai/response.rb b/lib/ai/response.rb index 059cd29..a1c98f5 100644 --- a/lib/ai/response.rb +++ b/lib/ai/response.rb @@ -8,5 +8,16 @@ def initialize(response) @usage = response.usage.to_h.slice(:input_tokens, :output_tokens, :total_tokens) @total_tokens = @usage[:total_tokens] end + + def to_hash + hash = { "#{self.class.name}" => {} } + + instance_variables.sort.each do |var| + value = instance_variable_get(var) + hash["#{self.class.name}"][var.to_s] = value unless value.nil? + end + + hash + end end end \ No newline at end of file From 88ccca56ef53f2bd44a1f2c0f64323cbd893bad7 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 30 Jul 2025 13:50:34 -0500 Subject: [PATCH 3/6] Improve amazing_print formatting for AI objects Replace hash-based formatting with object notation for better clarity. AI::Chat and AI::Response now display as proper objects with instance variables using @var: syntax. Features include: - Object notation (#) instead of hash format - Instance variables shown with colons (@messages:, @model:, etc) - Automatic content truncation for long messages (80 chars) - Sensitive data filtering (@api_key, @client hidden) - Proper indentation for nested objects and arrays This makes the output more intuitive for beginners who expect to see objects rather than hash representations. --- lib/ai-chat.rb | 9 ++++- lib/ai/amazing_print.rb | 81 +++++++++++++++++++++++++++++++++++++++++ lib/ai/chat.rb | 36 ++---------------- lib/ai/response.rb | 12 +----- lib/ai_chat.rb | 2 +- 5 files changed, 95 insertions(+), 45 deletions(-) create mode 100644 lib/ai/amazing_print.rb diff --git a/lib/ai-chat.rb b/lib/ai-chat.rb index f80aed4..6eaaa4e 100644 --- a/lib/ai-chat.rb +++ b/lib/ai-chat.rb @@ -1 +1,8 @@ -require_relative "ai/chat" \ No newline at end of file +require_relative "ai/chat" + +# Load amazing_print extension if amazing_print is available +begin + require_relative "ai/amazing_print" +rescue LoadError + # amazing_print not available, skip custom formatting +end diff --git a/lib/ai/amazing_print.rb b/lib/ai/amazing_print.rb new file mode 100644 index 0000000..5bd2f16 --- /dev/null +++ b/lib/ai/amazing_print.rb @@ -0,0 +1,81 @@ +require "amazing_print" + +module AmazingPrint + module AI + def self.included(base) + base.send :alias_method, :cast_without_ai, :cast + base.send :alias_method, :cast, :cast_with_ai + end + + def cast_with_ai(object, type) + case object + when ::AI::Chat, ::AI::Response + :ai_object + else + cast_without_ai(object, type) + end + end + + private + + def awesome_ai_object(object) + case object + when ::AI::Chat + format_ai_chat(object) + when ::AI::Response + format_ai_response(object) + else + awesome_object(object) + end + end + + def format_ai_chat(chat) + vars = [] + + # Format messages with truncation + if chat.instance_variable_defined?(:@messages) + messages = chat.instance_variable_get(:@messages).map do |msg| + truncated_msg = msg.dup + if msg[:content].is_a?(String) && msg[:content].length > 80 + truncated_msg[:content] = msg[:content][0..77] + "..." + end + truncated_msg + end + vars << ["@messages", messages] + end + + # Add other variables (except sensitive ones) + skip_vars = [:@api_key, :@client, :@messages] + chat.instance_variables.sort.each do |var| + next if skip_vars.include?(var) + value = chat.instance_variable_get(var) + vars << [var.to_s, value] unless value.nil? + end + + format_object(chat, vars) + end + + def format_ai_response(response) + vars = response.instance_variables.sort.map do |var| + [var.to_s, response.instance_variable_get(var)] + end + + format_object(response, vars) + end + + def format_object(object, vars) + data = vars.map do |(name, value)| + name = colorize(name, :variable) unless @options[:plain] + "#{name}: #{inspector.awesome(value)}" + end + + if @options[:multiline] + "#<#{object.class}\n#{data.map { |line| " #{line}" }.join("\n")}\n>" + else + "#<#{object.class} #{data.join(', ')}>" + end + end + end +end + +AmazingPrint::Formatter.send(:include, AmazingPrint::AI) \ No newline at end of file diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 1f0b603..dbcdf89 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -17,7 +17,7 @@ def self.loader(registry = Zeitwerk::Registry) attr_reader :reasoning_effort, :client VALID_REASONING_EFFORTS = [:low, :medium, :high].freeze - + def initialize(api_key: nil, api_key_env_var: "OPENAI_API_KEY") @api_key = api_key || ENV.fetch(api_key_env_var) @messages = [] @@ -97,7 +97,7 @@ def system(message) def user(message, image: nil, images: nil, file: nil, files: nil) add(message, role: "user", image: image, images: images, file: file, files: files) end - + def assistant(message, response: nil) add(message, role: "assistant", response: response) end @@ -147,7 +147,7 @@ def reasoning_effort=(value) end end end - + def schema=(value) if value.is_a?(String) @schema = JSON.parse(value, symbolize_names: true) @@ -181,34 +181,6 @@ def inspect "#<#{self.class.name} @messages=#{messages.inspect} @model=#{@model.inspect} @schema=#{@schema.inspect} @reasoning_effort=#{@reasoning_effort.inspect}>" end - def to_hash - hash = { "#{self.class.name}" => {} } - - # Define which instance variables to skip - skip_vars = [:@api_key, :@client] - - instance_variables.sort.each do |var| - next if skip_vars.include?(var) - - value = instance_variable_get(var) - - # Special handling for @messages to truncate content - if var == :@messages - value = value.map do |msg| - truncated_msg = msg.dup - if msg[:content].is_a?(String) && msg[:content].length > 80 - truncated_msg[:content] = msg[:content][0..77] + "..." - end - truncated_msg - end - end - - # Skip nil values for cleaner output - hash["#{self.class.name}"][var.to_s] = value unless value.nil? - end - - hash - end private @@ -302,7 +274,7 @@ def strip_responses(messages) def tools tools_list = [] if web_search - tools_list << { type: "web_search_preview" } + tools_list << { type: "web_search_preview" } end end diff --git a/lib/ai/response.rb b/lib/ai/response.rb index a1c98f5..b1af9ef 100644 --- a/lib/ai/response.rb +++ b/lib/ai/response.rb @@ -9,15 +9,5 @@ def initialize(response) @total_tokens = @usage[:total_tokens] end - def to_hash - hash = { "#{self.class.name}" => {} } - - instance_variables.sort.each do |var| - value = instance_variable_get(var) - hash["#{self.class.name}"][var.to_s] = value unless value.nil? - end - - hash - end end -end \ No newline at end of file +end diff --git a/lib/ai_chat.rb b/lib/ai_chat.rb index 3454b3b..0e928d4 100644 --- a/lib/ai_chat.rb +++ b/lib/ai_chat.rb @@ -3,4 +3,4 @@ loader.inflector.inflect("ai" => "AI") loader.setup - require_relative "ai/chat" \ No newline at end of file + require_relative "ai/chat" From 4247e43e670e1f4b8b2f3bc0d58a627137e246d8 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 30 Jul 2025 14:02:49 -0500 Subject: [PATCH 4/6] Add demo2/ to .gitignore Exclude temporary demo scripts from version control --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index bf62eba..05219fa 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,8 @@ vendor/bundle/ amazing_print/ openai-ruby/ +# Temp demo scripts +demo2/ + # OS files .DS_Store From 652cf88f136749e2e0baaee1cc56436c8443d999 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 30 Jul 2025 14:11:22 -0500 Subject: [PATCH 5/6] Add pretty_print support for Ruby's pp library Implement pretty_print methods for AI::Chat and AI::Response to provide clean output with Ruby's built-in pp command. Features: - Truncates long message content to 80 characters - Hides sensitive instance variables (@api_key, @client) - Displays objects with proper notation and indentation - Works alongside amazing_print for maximum flexibility --- lib/ai/chat.rb | 31 +++++++++++++++++++++++++++++++ lib/ai/response.rb | 11 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index dbcdf89..f6fa380 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -181,6 +181,37 @@ def inspect "#<#{self.class.name} @messages=#{messages.inspect} @model=#{@model.inspect} @schema=#{@schema.inspect} @reasoning_effort=#{@reasoning_effort.inspect}>" end + # Support for Ruby's pp (pretty print) + def pretty_print(q) + q.group(1, "#<#{self.class}", '>') do + q.breakable + + # Show messages with truncation + q.text "@messages=" + truncated_messages = @messages.map do |msg| + truncated_msg = msg.dup + if msg[:content].is_a?(String) && msg[:content].length > 80 + truncated_msg[:content] = msg[:content][0..77] + "..." + end + truncated_msg + end + q.pp truncated_messages + + # Show other instance variables (except sensitive ones) + skip_vars = [:@messages, :@api_key, :@client] + instance_variables.sort.each do |var| + next if skip_vars.include?(var) + value = instance_variable_get(var) + unless value.nil? + q.text "," + q.breakable + q.text "#{var}=" + q.pp value + end + end + end + end + private diff --git a/lib/ai/response.rb b/lib/ai/response.rb index b1af9ef..5722222 100644 --- a/lib/ai/response.rb +++ b/lib/ai/response.rb @@ -9,5 +9,16 @@ def initialize(response) @total_tokens = @usage[:total_tokens] end + # Support for Ruby's pp (pretty print) + def pretty_print(q) + q.group(1, "#<#{self.class}", '>') do + instance_variables.sort.each_with_index do |var, i| + q.breakable + q.text "#{var}=" + q.pp instance_variable_get(var) + q.text "," if i < instance_variables.length - 1 + end + end + end end end From 6e63430d663c555bdefe0faeb421b259ddf81d79 Mon Sep 17 00:00:00 2001 From: Jelani Woods Date: Thu, 14 Aug 2025 23:11:32 -0500 Subject: [PATCH 6/6] Resolve inconsistencies --- README.md | 2 +- examples/10_additional_patterns.rb | 2 +- examples/12_image_generation.rb | 4 ++-- spec/integration/ai_chat_integration_spec.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7fc4e96..6e29c5d 100644 --- a/README.md +++ b/README.md @@ -599,7 +599,7 @@ You can also, if you know a response ID, continue an old conversation by setting t = AI::Chat.new t.user("Hello!") t.generate! -old_id = t.last[:response].id # => "resp_abc123..." +old_id = t.last[:response][:id] # => "resp_abc123..." # Some time in the future... diff --git a/examples/10_additional_patterns.rb b/examples/10_additional_patterns.rb index a0029b6..378843c 100644 --- a/examples/10_additional_patterns.rb +++ b/examples/10_additional_patterns.rb @@ -79,7 +79,7 @@ puts "✓ First chat with web search: #{response[0..100]}..." # Chain to second chat - response_id = chat4a.last[:response].id + response_id = chat4a.last.dig(:response, :id) chat4b = AI::Chat.new chat4b.model = "gpt-4o" chat4b.previous_response_id = response_id diff --git a/examples/12_image_generation.rb b/examples/12_image_generation.rb index 98a56ff..1ff0913 100755 --- a/examples/12_image_generation.rb +++ b/examples/12_image_generation.rb @@ -22,8 +22,8 @@ # Access images through the response object response_obj = a.messages.last[:response] -puts "Response ID: #{response_obj.id}" -puts "Images via response object: #{response_obj.images}" +puts "Response ID: #{response_obj[:id]}" +puts "Images via response object: #{response_obj[:images]}" puts puts "Example 2: Model remembers previously generated images" diff --git a/spec/integration/ai_chat_integration_spec.rb b/spec/integration/ai_chat_integration_spec.rb index a84662a..3b2fd3d 100644 --- a/spec/integration/ai_chat_integration_spec.rb +++ b/spec/integration/ai_chat_integration_spec.rb @@ -254,7 +254,7 @@ response_obj = chat.last[:response] expect(response_obj).to be_a(Hash) - expect(response_obj.id).to match(/^resp_/) + expect(response_obj[:id]).to match(/^resp_/) expect(response_obj[:model]).to be_a(String) expect(response_obj[:usage]).to be_a(Hash) expect(response_obj[:usage][:total_tokens]).to be_a(Integer)