Action TextのRichTextオブジェクトをLLMメッセージに変換する際のHTMLタグ混入を修正

crmne/ruby_llm

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_contentActionText::RichText を受け取った場合に to_plain_text を呼び出すよう、acts_as_legacy.rbmessage_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_contentRubyLLM::Content.new に渡すことで、テキストとファイル添付を組み合わせたマルチモーダルなペイロードも正しく構築されます。

テスト面では spec/ruby_llm/active_record/acts_as_action_text_spec.rb が新たに追加されました。ActionText::RichTextinstance_double を用いたモックを使い、「通常のString」「Action Textのみ」「Action Text+添付ファイル」の3ケースをカバーしています。また spec/dummy/config/application.rbrequire 'action_text/engine' が追加され、テスト環境でAction Textが読み込まれるようになっています。

設計判断

ダックタイピングによる検出 が採用されており、ActionText::RichText クラスへの直接の依存を避けた実装になっています。content.respond_to?(:to_plain_text) という判定は、Action Textが利用可能かどうかを問わず、to_plain_text を持つ任意のオブジェクトに対して正しく動作します。Action Textを使わないアプリケーションでは content が通常の String のままであり、Stringto_plain_text を持たないため既存の動作は変わりません。

acts_as_legacy.rbmessage_methods.rb の両ファイルに修正が加えられている点からも、この変換ロジックはどちらの実装パスを経由しても一貫して適用される設計が意識されています。

まとめ

この修正により、has_rich_text を使うメッセージモデルでも追加設定なしにRubyLLMとの連携が正しく機能するようになりました。respond_to?(:to_plain_text) によるダックタイピングは後方互換性を維持しつつAction Textへの対応を実現しており、ライブラリ側でユースケースを吸収する堅実なアプローチといえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
d72946f9

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)」という理想的な3部構成が明確に適用されています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:パス)とPR番号のリンク記法([#番号](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

RailsのAction Textや内部実装に関する知識を前提としており、専門知識を持つエンジニアという対象読者に適合した内容です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロック(acts_as_legacy.rb, message_methods.rb)は、提供されたDiffの内容と正確に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ActionText::RichText」「ダックタイピング」「self[:content] vs content」などの技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「self[:content]ではActionTextオブジェクトにアクセスできない」という説明など、技術的な解説が正確で論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescription、Diff、または技術的に自明な内容で裏付けられており、ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#365)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの内容を具体的かつ正確に表現しています。PRのチェックボックスで「Bug fix」が選択されている点とも整合性が取れています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョン情報やリリース予定など、PR情報にない外部知識の追加は見られず、記事内容は提供された情報に基づいています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「既に」「将来的に」といった時間表現の歪曲や誤用は見られません。