Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ vendor/bundle/
amazing_print/
openai-ruby/

# Temp demo scripts
demo2/

# OS files
.DS_Store

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 => #<AI::Chat::Response id=resp_abc... model=gpt-4.1-nano tokens=12>}
# {: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
Expand All @@ -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 => #<AI::Chat::Response id=resp_abc... model=gpt-4.1-nano tokens=12>}
{:role => "assistant", :content => "Hi there! How can I help you today?", :response => { id=resp_abc... model=gpt-4.1-nano tokens=12 } }
]
```

Expand Down Expand Up @@ -577,7 +577,7 @@ pp t.messages.last
# => {
# :role => "assistant",
# :content => "Hello! How can I help you today?",
# :response => #<AI::Response id=resp_abc... model=gpt-4.1-nano tokens=12>
# :response => { id=resp_abc... model=gpt-4.1-nano tokens=12 }
# }

# Access detailed information
Expand All @@ -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...

Expand Down
2 changes: 1 addition & 1 deletion examples/10_additional_patterns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions examples/12_image_generation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions lib/ai-chat.rb
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions lib/ai/amazing_print.rb
Original file line number Diff line number Diff line change
@@ -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)
34 changes: 33 additions & 1 deletion lib/ai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cleaner formatting, consider filtering out sensitive instance variables (e.g. using reject) and using q.seplist to automatically handle comma separation instead of manual iteration with each index.

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
Expand Down Expand Up @@ -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)..] || []
Expand Down
14 changes: 7 additions & 7 deletions spec/integration/ai_chat_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading