Class: AssistantMessageLinkifyHandler
- Inherits:
-
Object
- Object
- AssistantMessageLinkifyHandler
- Defined in:
- app/subscribers/assistant_message_linkify_handler.rb
Overview
Post-processes assistant LLM responses to ensure CRM reference numbers
(CN#, ON#, SO#, SQ#) are correctly linked.
Two responsibilities:
- Linkify plain-text references the LLM left unlinked
- Validate LLM-generated CRM reference links — if the link text is a
known reference but the URL is wrong (e.g. /searches/...), fix the URL
Canonical URL patterns:
CN12345 → CN12345 (CN embeds customer id)
ON… → ON… via Crm::OpportunityLinkPath
SO12345 → SO12345
SQ12345 → SQ12345
Non-CRM links (blog posts, product pages, source citations) are never touched.
Constant Summary collapse
- MARKDOWN_LINK =
/\[([^\]]*)\]\(([^)]*)\)/- CRM_REF =
/\A(CN|ON|SO|SQ)(\d+)\z/i- CORRECT_PATH_SUFFIX =
{ 'CN' => ->(id) { "/customers/#{id}" }, 'SO' => ->(id) { "/orders/SO#{id}" }, 'SQ' => ->(id) { "/quotes/SQ#{id}" } }.freeze
Instance Method Summary collapse
Instance Method Details
#call(event) ⇒ Object
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'app/subscribers/assistant_message_linkify_handler.rb', line 29 def call(event) msg = AssistantMessage.find_by(id: event.data[:assistant_message_id]) return unless msg&.content.present? base = CRM_URL placeholders = [] # Mask fenced code blocks and inline code so CRM refs inside them aren't rewritten. masked = msg.content.gsub(/(`{3,})[^`]*?\1|`[^`\n]+`/m) do |match| placeholders << match "\x00LINK#{placeholders.size - 1}\x00" end # Mask all markdown links. CRM reference links get validated; others pass through. masked.gsub!(MARKDOWN_LINK) do text = $1 url = $2 corrected = validate_crm_link(text, url, base) placeholders << corrected "\x00LINK#{placeholders.size - 1}\x00" end # Linkify plain-text references the LLM left unlinked. masked.gsub!(/\bCN(\d+)\b/i) { "[CN#{$1}](#{base}/customers/#{$1})" } masked.gsub!(/\b(ON\d+)\b/i) do |full| digits = full[/ON(\d+)/i, 1] "[#{full}](#{base}#{Crm::OpportunityLinkPath.path_for_on_digits(digits)})" end masked.gsub!(/\b(SO\d+)\b/i) { "[#{$1}](#{base}/orders/#{$1})" } masked.gsub!(/\b(SQ\d+)\b/i) { "[#{$1}](#{base}/quotes/#{$1})" } # Restore all masked content (code blocks, validated links, untouched non-CRM links). linked = masked.gsub(/\x00LINK(\d+)\x00/) { placeholders[$1.to_i] } msg.update_column(:content, linked) if linked != msg.content end |