diff --git a/.gitignore b/.gitignore index 59dde80..9924405 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ vendor/bundle/ amazing_print/ openai-ruby/ +# Temp demo scripts +demo2/ + # OS files .DS_Store diff --git a/README.md b/README.md index 3bad091..6e29c5d 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ a.generate! # => "Matz is nice and so we are nice" (or similar) pp a.messages # => [ # {:role=>"user", :content=>"If the Ruby community had an official motto, what might it be?"}, -# {:role=>"assistant", :content=>"Matz is nice and so we are nice", :response => #} +# {:role=>"assistant", :content=>"Matz is nice and so we are nice", :response => { id=resp_abc... model=gpt-4.1-nano tokens=12 } } # ] # Continue the conversation @@ -108,7 +108,7 @@ That's it! You're building something like this: [ {:role => "system", :content => "You are a helpful assistant"}, {:role => "user", :content => "Hello!"}, - {:role => "assistant", :content => "Hi there! How can I help you today?", :response => #} + {:role => "assistant", :content => "Hi there! How can I help you today?", :response => { id=resp_abc... model=gpt-4.1-nano tokens=12 } } ] ``` @@ -577,7 +577,7 @@ pp t.messages.last # => { # :role => "assistant", # :content => "Hello! How can I help you today?", -# :response => # +# :response => { id=resp_abc... model=gpt-4.1-nano tokens=12 } # } # Access detailed information @@ -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/lib/ai-chat.rb b/lib/ai-chat.rb index 101cf28..6eaaa4e 100644 --- a/lib/ai-chat.rb +++ b/lib/ai-chat.rb @@ -1 +1,8 @@ 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..aa231ca --- /dev/null +++ b/lib/ai/amazing_print.rb @@ -0,0 +1,71 @@ +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_object + else + cast_without_ai(object, type) + end + end + + private + + def awesome_ai_object(object) + case object + when ::AI::Chat + format_ai_chat(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_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) diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 45e8d1b..a0ccf40 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -176,6 +176,38 @@ 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 class InputClassificationError < StandardError; end @@ -213,7 +245,7 @@ def create_response def prepare_messages_for_api return messages unless previous_response_id - previous_response_index = messages.find_index { |message| message[:response]&.id == previous_response_id } + previous_response_index = messages.find_index { |message| message.dig(:response, :id) == previous_response_id } if previous_response_index messages[(previous_response_index + 1)..] || [] diff --git a/spec/integration/ai_chat_integration_spec.rb b/spec/integration/ai_chat_integration_spec.rb index 1f9e6f8..3b2fd3d 100644 --- a/spec/integration/ai_chat_integration_spec.rb +++ b/spec/integration/ai_chat_integration_spec.rb @@ -14,7 +14,7 @@ expect(response).to match(/4|four/i) expect(chat.messages.count).to eq(2) expect(chat.messages.last[:role]).to eq("assistant") - expect(chat.messages.last[:response]).to be_a(AI::Response) + expect(chat.messages.last[:response]).to be_a(Hash) end it "maintains conversation context across multiple turns" do @@ -253,12 +253,12 @@ response_obj = chat.last[:response] - expect(response_obj).to be_a(AI::Response) - 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) - expect(response_obj.total_tokens).to eq(response_obj.usage[:total_tokens]) + expect(response_obj).to be_a(Hash) + 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) + expect(response_obj[:total_tokens]).to eq(response_obj[:usage][:total_tokens]) end end