Skip to content
10 changes: 8 additions & 2 deletions lib/ruby_llm/active_record/acts_as_legacy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
10 changes: 8 additions & 2 deletions lib/ruby_llm/active_record/message_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
62 changes: 62 additions & 0 deletions spec/ruby_llm/active_record/acts_as_action_text_spec.rb
Original file line number Diff line number Diff line change
@@ -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