From 8c3fd8d2df1d9320c6921110266a5dfb3e189951 Mon Sep 17 00:00:00 2001 From: mgc Date: Sun, 24 Aug 2025 21:43:11 -0700 Subject: [PATCH 1/7] Feat: Add support to Action Text enabled content Improved the `extract_content` method to correctly process Action Text. When `message` model uses `has_rich_text :content`, `message.content` becomes `ActionText:RichText`, returning HTML tags unintentionally. Adjustments ensure only the desired content is extracted. --- lib/ruby_llm/active_record/acts_as.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index d4b8984f1..dbbb4c5aa 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -353,9 +353,15 @@ def extract_tool_call_id end def extract_content - return content unless respond_to?(:attachments) && attachments.attached? + text_content = if content.respond_to?(:to_plain_text) + content.to_plain_text + else + content.to_s + end - RubyLLM::Content.new(content).tap do |content_obj| + return text_content unless respond_to?(:attachments) && attachments.attached? + + RubyLLM::Content.new(text_content).tap do |content_obj| @_tempfiles = [] attachments.each do |attachment| From 4fe8b12d50cf9d1df317462f06883d470d97ed44 Mon Sep 17 00:00:00 2001 From: mgc Date: Fri, 29 Aug 2025 20:39:12 -0700 Subject: [PATCH 2/7] Rspec: Add tests for content conversion --- spec/dummy/config/application.rb | 1 + spec/ruby_llm/active_record/acts_as_spec.rb | 23 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 9ed32e3c9..a0be46f9e 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -7,6 +7,7 @@ require 'active_record/railtie' require 'active_storage/engine' require 'action_controller/railtie' +require 'action_text/engine' Bundler.require(*Rails.groups) require 'ruby_llm' diff --git a/spec/ruby_llm/active_record/acts_as_spec.rb b/spec/ruby_llm/active_record/acts_as_spec.rb index 458326766..b0496f983 100644 --- a/spec/ruby_llm/active_record/acts_as_spec.rb +++ b/spec/ruby_llm/active_record/acts_as_spec.rb @@ -378,6 +378,29 @@ class BotToolCall < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinition end end + describe 'Action Text content support' do + it 'converts Action Text content to plain text' do + chat = Chat.create!(model_id: model) + action_text_content = instance_double(ActionText::RichText) + allow(action_text_content).to receive(:to_plain_text).and_return('This is rich text content') + + message = chat.messages.create!(role: 'user') + allow(message).to receive(:content).and_return(action_text_content) + + llm_message = message.to_llm + + expect(action_text_content).to have_received(:to_plain_text) + expect(llm_message.content).to eq('This is rich text content') + end + + it 'handles regular string content when to_plain_text is not available' do + chat = Chat.create!(model_id: model) + message = chat.messages.create!(role: 'user', content: 'Regular text content') + llm_message = message.to_llm + expect(llm_message.content).to eq('Regular text content') + end + end + describe 'attachment handling' do let(:image_path) { File.expand_path('../../fixtures/ruby.png', __dir__) } let(:pdf_path) { File.expand_path('../../fixtures/sample.pdf', __dir__) } From c69ab276d5feb96033a2803e5409935320e7f153 Mon Sep 17 00:00:00 2001 From: mgc Date: Sat, 30 Aug 2025 11:54:47 -0700 Subject: [PATCH 3/7] Test: Add unit tests for Action Text attachment handling --- spec/ruby_llm/active_record/acts_as_spec.rb | 38 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/spec/ruby_llm/active_record/acts_as_spec.rb b/spec/ruby_llm/active_record/acts_as_spec.rb index b0496f983..eefae6fbf 100644 --- a/spec/ruby_llm/active_record/acts_as_spec.rb +++ b/spec/ruby_llm/active_record/acts_as_spec.rb @@ -379,26 +379,54 @@ class BotToolCall < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinition end describe 'Action Text content support' do - it 'converts Action Text content to plain text' do - chat = Chat.create!(model_id: model) - action_text_content = instance_double(ActionText::RichText) - allow(action_text_content).to receive(:to_plain_text).and_return('This is rich text content') + let(:chat) { Chat.create!(model_id: model) } + + def mock_action_text(plain_text) + instance_double(ActionText::RichText).tap do |mock| + allow(mock).to receive(:to_plain_text).and_return(plain_text) + end + end + it 'converts Action Text content to plain text' do + action_text_content = mock_action_text('This is rich text content') message = chat.messages.create!(role: 'user') allow(message).to receive(:content).and_return(action_text_content) llm_message = message.to_llm + expect(message.content).not_to be_a(String) expect(action_text_content).to have_received(:to_plain_text) expect(llm_message.content).to eq('This is rich text content') end it 'handles regular string content when to_plain_text is not available' do - chat = Chat.create!(model_id: model) message = chat.messages.create!(role: 'user', content: 'Regular text content') + llm_message = message.to_llm + + expect(message.content).to be_a(String) expect(llm_message.content).to eq('Regular text content') end + + it 'handles Action Text content with attachments' do + action_text_content = mock_action_text('Rich text with attachment reference') + message = chat.messages.create!(role: 'user') + allow(message).to receive(:content).and_return(action_text_content) + + message.attachments.attach( + io: StringIO.new('test data'), + filename: 'test.txt', + content_type: 'text/plain' + ) + + llm_message = message.to_llm + + expect(action_text_content).to have_received(:to_plain_text) + expect(llm_message.content).to be_a(RubyLLM::Content) + expect(llm_message.content.text).to eq('Rich text with attachment reference') + expect(llm_message.content.attachments).not_to be_empty + expect(llm_message.content.attachments.first.mime_type).to eq('text/plain') + end end describe 'attachment handling' do From 99ebf44fb7dfc72cf814256084d01327f9f884ae Mon Sep 17 00:00:00 2001 From: mgc Date: Sun, 31 Aug 2025 15:27:22 -0700 Subject: [PATCH 4/7] Revert "Test: Add unit tests for Action Text attachment handling" This reverts commit c69ab276d5feb96033a2803e5409935320e7f153. --- spec/ruby_llm/active_record/acts_as_spec.rb | 38 +++------------------ 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/spec/ruby_llm/active_record/acts_as_spec.rb b/spec/ruby_llm/active_record/acts_as_spec.rb index eefae6fbf..b0496f983 100644 --- a/spec/ruby_llm/active_record/acts_as_spec.rb +++ b/spec/ruby_llm/active_record/acts_as_spec.rb @@ -379,54 +379,26 @@ class BotToolCall < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinition end describe 'Action Text content support' do - let(:chat) { Chat.create!(model_id: model) } - - def mock_action_text(plain_text) - instance_double(ActionText::RichText).tap do |mock| - allow(mock).to receive(:to_plain_text).and_return(plain_text) - end - end - it 'converts Action Text content to plain text' do - action_text_content = mock_action_text('This is rich text content') + chat = Chat.create!(model_id: model) + action_text_content = instance_double(ActionText::RichText) + allow(action_text_content).to receive(:to_plain_text).and_return('This is rich text content') + message = chat.messages.create!(role: 'user') allow(message).to receive(:content).and_return(action_text_content) llm_message = message.to_llm - expect(message.content).not_to be_a(String) expect(action_text_content).to have_received(:to_plain_text) expect(llm_message.content).to eq('This is rich text content') end it 'handles regular string content when to_plain_text is not available' do + chat = Chat.create!(model_id: model) message = chat.messages.create!(role: 'user', content: 'Regular text content') - llm_message = message.to_llm - - expect(message.content).to be_a(String) expect(llm_message.content).to eq('Regular text content') end - - it 'handles Action Text content with attachments' do - action_text_content = mock_action_text('Rich text with attachment reference') - message = chat.messages.create!(role: 'user') - allow(message).to receive(:content).and_return(action_text_content) - - message.attachments.attach( - io: StringIO.new('test data'), - filename: 'test.txt', - content_type: 'text/plain' - ) - - llm_message = message.to_llm - - expect(action_text_content).to have_received(:to_plain_text) - expect(llm_message.content).to be_a(RubyLLM::Content) - expect(llm_message.content.text).to eq('Rich text with attachment reference') - expect(llm_message.content.attachments).not_to be_empty - expect(llm_message.content.attachments.first.mime_type).to eq('text/plain') - end end describe 'attachment handling' do From cd417277aca4d0970fe8eba3f7e625aba0994233 Mon Sep 17 00:00:00 2001 From: mgc Date: Sun, 31 Aug 2025 15:27:25 -0700 Subject: [PATCH 5/7] Revert "Rspec: Add tests for content conversion" This reverts commit 4fe8b12d50cf9d1df317462f06883d470d97ed44. --- spec/dummy/config/application.rb | 1 - spec/ruby_llm/active_record/acts_as_spec.rb | 23 --------------------- 2 files changed, 24 deletions(-) diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index a0be46f9e..9ed32e3c9 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -7,7 +7,6 @@ require 'active_record/railtie' require 'active_storage/engine' require 'action_controller/railtie' -require 'action_text/engine' Bundler.require(*Rails.groups) require 'ruby_llm' diff --git a/spec/ruby_llm/active_record/acts_as_spec.rb b/spec/ruby_llm/active_record/acts_as_spec.rb index b0496f983..458326766 100644 --- a/spec/ruby_llm/active_record/acts_as_spec.rb +++ b/spec/ruby_llm/active_record/acts_as_spec.rb @@ -378,29 +378,6 @@ class BotToolCall < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinition end end - describe 'Action Text content support' do - it 'converts Action Text content to plain text' do - chat = Chat.create!(model_id: model) - action_text_content = instance_double(ActionText::RichText) - allow(action_text_content).to receive(:to_plain_text).and_return('This is rich text content') - - message = chat.messages.create!(role: 'user') - allow(message).to receive(:content).and_return(action_text_content) - - llm_message = message.to_llm - - expect(action_text_content).to have_received(:to_plain_text) - expect(llm_message.content).to eq('This is rich text content') - end - - it 'handles regular string content when to_plain_text is not available' do - chat = Chat.create!(model_id: model) - message = chat.messages.create!(role: 'user', content: 'Regular text content') - llm_message = message.to_llm - expect(llm_message.content).to eq('Regular text content') - end - end - describe 'attachment handling' do let(:image_path) { File.expand_path('../../fixtures/ruby.png', __dir__) } let(:pdf_path) { File.expand_path('../../fixtures/sample.pdf', __dir__) } From 532a55b2967a22428e8f09e1b6caf4d9f26087ef Mon Sep 17 00:00:00 2001 From: mgc Date: Sun, 31 Aug 2025 15:27:26 -0700 Subject: [PATCH 6/7] Revert "Feat: Add support to Action Text enabled content" This reverts commit 8c3fd8d2df1d9320c6921110266a5dfb3e189951. --- lib/ruby_llm/active_record/acts_as.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index dbbb4c5aa..d4b8984f1 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -353,15 +353,9 @@ def extract_tool_call_id end def extract_content - text_content = if content.respond_to?(:to_plain_text) - content.to_plain_text - else - content.to_s - end + return content unless respond_to?(:attachments) && attachments.attached? - return text_content unless respond_to?(:attachments) && attachments.attached? - - RubyLLM::Content.new(text_content).tap do |content_obj| + RubyLLM::Content.new(content).tap do |content_obj| @_tempfiles = [] attachments.each do |attachment| From c96c23648f4d6b06c59037c0e7d828655af2cd29 Mon Sep 17 00:00:00 2001 From: mgc Date: Sun, 31 Aug 2025 16:08:42 -0700 Subject: [PATCH 7/7] Feat: Enhance content extraction with Action Text --- lib/ruby_llm/active_record/acts_as_legacy.rb | 10 ++- lib/ruby_llm/active_record/message_methods.rb | 10 ++- spec/dummy/config/application.rb | 1 + .../active_record/acts_as_action_text_spec.rb | 62 +++++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 spec/ruby_llm/active_record/acts_as_action_text_spec.rb diff --git a/lib/ruby_llm/active_record/acts_as_legacy.rb b/lib/ruby_llm/active_record/acts_as_legacy.rb index 97679c126..97c3d9776 100644 --- a/lib/ruby_llm/active_record/acts_as_legacy.rb +++ b/lib/ruby_llm/active_record/acts_as_legacy.rb @@ -368,9 +368,15 @@ def extract_tool_call_id end def extract_content - return content unless respond_to?(:attachments) && attachments.attached? + text_content = if content.respond_to?(:to_plain_text) + content.to_plain_text + else + content.to_s + end - RubyLLM::Content.new(content).tap do |content_obj| + return text_content unless respond_to?(:attachments) && attachments.attached? + + RubyLLM::Content.new(text_content).tap do |content_obj| @_tempfiles = [] attachments.each do |attachment| diff --git a/lib/ruby_llm/active_record/message_methods.rb b/lib/ruby_llm/active_record/message_methods.rb index d5ec8e4e9..780310d7c 100644 --- a/lib/ruby_llm/active_record/message_methods.rb +++ b/lib/ruby_llm/active_record/message_methods.rb @@ -42,9 +42,15 @@ def extract_tool_call_id end def extract_content - return content unless respond_to?(:attachments) && attachments.attached? + text_content = if content.respond_to?(:to_plain_text) + content.to_plain_text + else + content.to_s + end - RubyLLM::Content.new(content).tap do |content_obj| + return text_content unless respond_to?(:attachments) && attachments.attached? + + RubyLLM::Content.new(text_content).tap do |content_obj| @_tempfiles = [] attachments.each do |attachment| diff --git a/spec/dummy/config/application.rb b/spec/dummy/config/application.rb index 062a7c21b..9fb47522f 100644 --- a/spec/dummy/config/application.rb +++ b/spec/dummy/config/application.rb @@ -7,6 +7,7 @@ require 'active_record/railtie' require 'active_storage/engine' require 'action_controller/railtie' +require 'action_text/engine' Bundler.require(*Rails.groups) require 'ruby_llm' diff --git a/spec/ruby_llm/active_record/acts_as_action_text_spec.rb b/spec/ruby_llm/active_record/acts_as_action_text_spec.rb new file mode 100644 index 000000000..d22648c26 --- /dev/null +++ b/spec/ruby_llm/active_record/acts_as_action_text_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RubyLLM::ActiveRecord::ActsAs do + include_context 'with configured RubyLLM' + + let(:model) { 'gpt-4.1-nano' } + let(:chat) { Chat.create!(model: model) } + + def mock_action_text(plain_text) + instance_double(ActionText::RichText).tap do |mock| + allow(mock).to receive(:to_plain_text).and_return(plain_text) + end + end + + def create_message_with_action_text(content_text) + message = chat.messages.create!(role: :user) + action_text_content = mock_action_text(content_text) + allow(message).to receive(:content).and_return(action_text_content) + [message, action_text_content] + end + + describe 'Action Text content extraction' do + context 'when content responds to to_plain_text' do + it 'extracts plain text from Action Text content' do + message, action_text_content = create_message_with_action_text('This is plain text') + + llm_message = message.to_llm + + expect(action_text_content).to have_received(:to_plain_text) + expect(llm_message.content).to eq('This is plain text') + end + end + + context 'when content is a regular string' do + it 'returns content unchanged' do + message = chat.messages.create!(role: :user, content: 'Regular text content') + + expect(message.to_llm.content).to eq('Regular text content') + end + end + + context 'when Action Text content has attachments' do + let(:test_attachment) do + { io: StringIO.new('test data'), filename: 'test.txt', content_type: 'text/plain' } + end + + it 'combines Action Text with attachments into RubyLLM::Content' do + message, action_text_content = create_message_with_action_text('Rich text with attachment') + message.attachments.attach(test_attachment) + + llm_message = message.to_llm + + expect(action_text_content).to have_received(:to_plain_text) + expect(llm_message.content).to be_a(RubyLLM::Content) + expect(llm_message.content.text).to eq('Rich text with attachment') + expect(llm_message.content.attachments.first.mime_type).to eq('text/plain') + end + end + end +end