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