Class: AssistantMessageLinkifyHandler

Inherits:
Object
  • Object
show all
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:

  1. Linkify plain-text references the LLM left unlinked
  2. 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

/\[([^\]]*)\]\(([^)]*)\)/
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