Action TextのRichTextオブジェクトをLLMメッセージに変換する際のHTMLタグ混入を修正
Railsの has_rich_text を使ったメッセージモデルで extract_content を呼び出すと、ActionText::RichText オブジェクトがそのまま文字列化され、HTMLタグが混入する問題を修正しました。to_plain_text の有無を検出して平文を抽出することで、Action TextとLLMの連携が正しく機能するようになります。
背景
acts_as_message を使うメッセージモデルに has_rich_text :content を追加すると、message.content の返り値が String から ActionText::RichText に変わります。ActionText::RichText はRailsのActive Recordオブジェクトであり、to_s を呼び出すとHTMLが含まれた文字列を返します。
RubyLLMの extract_content はこれを想定しておらず、content を素通りでLLMへのペイロードに渡していました。その結果、<div class="trix-content"><p>こんにちは</p></div> のようなHTML文字列がLLMに送信され、意図しない動作を引き起こす可能性がありました。
技術的な変更
extract_content が ActionText::RichText を受け取った場合に to_plain_text を呼び出すよう、acts_as_legacy.rb と message_methods.rb の両実装に対して修正が加えられました。
acts_as_legacy.rb の変更前:
def extract_content
return content unless respond_to?(:attachments) && attachments.attached?
RubyLLM::Content.new(content).tap do |content_obj|
変更後:
def extract_content
text_content = if content.respond_to?(:to_plain_text)
content.to_plain_text
else
content
end
return text_content unless respond_to?(:attachments) && attachments.attached?
RubyLLM::Content.new(text_content).tap do |content_obj|
message_methods.rb の変更前:
content_value = self[:content]
変更後:
content_value = content
content_value = content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
なお message_methods.rb では self[:content](生のデータベース値を読む)から content(Railsのアクセサ経由)への変更も同時に行われています。has_rich_text はアクセサを上書きするため、self[:content] では ActionText::RichText オブジェクトに到達できず、そもそもAction Textの検出ができないという問題がありました。
アタッチメントが存在するケースも考慮されており、平文化した text_content を RubyLLM::Content.new に渡すことで、テキストとファイル添付を組み合わせたマルチモーダルなペイロードも正しく構築されます。
テスト面では spec/ruby_llm/active_record/acts_as_action_text_spec.rb が新たに追加されました。ActionText::RichText の instance_double を用いたモックを使い、「通常のString」「Action Textのみ」「Action Text+添付ファイル」の3ケースをカバーしています。また spec/dummy/config/application.rb に require 'action_text/engine' が追加され、テスト環境でAction Textが読み込まれるようになっています。
設計判断
ダックタイピングによる検出 が採用されており、ActionText::RichText クラスへの直接の依存を避けた実装になっています。content.respond_to?(:to_plain_text) という判定は、Action Textが利用可能かどうかを問わず、to_plain_text を持つ任意のオブジェクトに対して正しく動作します。Action Textを使わないアプリケーションでは content が通常の String のままであり、String は to_plain_text を持たないため既存の動作は変わりません。
acts_as_legacy.rb と message_methods.rb の両ファイルに修正が加えられている点からも、この変換ロジックはどちらの実装パスを経由しても一貫して適用される設計が意識されています。
まとめ
この修正により、has_rich_text を使うメッセージモデルでも追加設定なしにRubyLLMとの連携が正しく機能するようになりました。respond_to?(:to_plain_text) によるダックタイピングは後方互換性を維持しつつAction Textへの対応を実現しており、ライブラリ側でユースケースを吸収する堅実なアプローチといえます。