Class: Assistant::ResponseFormatter

Inherits:
Object
  • Object
show all
Includes:
ActionView::Helpers::DateHelper, ActionView::Helpers::NumberHelper, ActionView::Helpers::UrlHelper
Defined in:
app/services/assistant/response_formatter.rb

Overview

Formats AI chat responses for display

Handles:

  • Markdown to HTML conversion using Kramdown (GFM-compatible)
  • SQL code blocks in collapsible accordions
  • Table formatting with proper number/date/currency formatting
  • Entity linking (customers, employees, products, orders)
  • Copy to clipboard buttons

Usage:
formatter = Assistant::ResponseFormatter.new(content, view_context: view_context)
html = formatter.format

Constant Summary collapse

CURRENCY_COLUMNS =

Fallback heuristics for AI-generated column headers that have no manifest metadata.
These are only used when the column wasn't found in @column_metadata (i.e. the AI
aliased or computed the column, or the table isn't from a SQL tool result).
For SQL tool results, manifest-backed column_hints take precedence (see format_cell).

/revenue|amount|value|price|cost|msrp/i
DATE_COLUMNS =
/date|created_at|updated_at|shipped|ordered/i
PERCENTAGE_COLUMNS =
/rate|percent|pct|ratio/i
COUNT_COLUMNS =
/count|orders|units|quantity|calls|activities|stock|inventory|qty/i
PRODUCT_LINE_COLUMNS =
/product_line|product line|productline/i
LANGUAGE_DISPLAY_NAMES =

Human-friendly names for code block language identifiers

{
  'html' => 'HTML', 'css' => 'CSS', 'scss' => 'SCSS', 'sass' => 'Sass',
  'javascript' => 'JavaScript', 'js' => 'JavaScript',
  'typescript' => 'TypeScript', 'ts' => 'TypeScript',
  'ruby' => 'Ruby', 'rb' => 'Ruby', 'erb' => 'ERB',
  'sql' => 'SQL', 'json' => 'JSON', 'yaml' => 'YAML', 'yml' => 'YAML',
  'python' => 'Python', 'py' => 'Python',
  'bash' => 'Bash', 'sh' => 'Shell', 'shell' => 'Shell', 'zsh' => 'Shell',
  'xml' => 'XML', 'svg' => 'SVG',
  'markdown' => 'Markdown', 'md' => 'Markdown',
  'text' => 'Plain Text', 'plaintext' => 'Plain Text',
  'csv' => 'CSV', 'diff' => 'Diff', 'go' => 'Go', 'rust' => 'Rust',
  'java' => 'Java', 'swift' => 'Swift', 'kotlin' => 'Kotlin',
  'php' => 'PHP', 'c' => 'C', 'cpp' => 'C++', 'csharp' => 'C#'
}.freeze
SANITIZE_CONFIG =

Sanitize LLM HTML to strip XSS vectors while keeping safe formatting.
Runs BEFORE our post-processing (tables, SQL blocks) which adds safe data-* attributes.
Based on Sanitize::Config::RELAXED minus tags and inline style attributes.

Sanitize::Config.merge(
  Sanitize::Config::RELAXED,
  elements: Sanitize::Config::RELAXED[:elements] - ['style'],
  attributes: Sanitize::Config.merge(
    Sanitize::Config::RELAXED[:attributes],
    all: (Sanitize::Config::RELAXED[:attributes][:all] || Set.new) - Set['style']
  )
).freeze

Instance Method Summary collapse

Constructor Details

#initialize(content, view_context: nil, column_metadata: {}) ⇒ ResponseFormatter

Returns a new instance of ResponseFormatter.

Parameters:

  • content (String)

    Raw markdown content from the AI

  • view_context (ActionView::Base, nil) (defaults to: nil)

    For URL helpers (entity linking)

  • column_metadata (Hash) (defaults to: {})

    Column formatting hints from SqlBroker
    e.g. { "qty_on_hand" => "integer", "unit_cogs" => "currency", "gl_date" => "date" }
    Hint values: 'currency', 'integer', 'number', 'percentage', 'date', 'boolean', 'text'



69
70
71
72
73
74
75
# File 'app/services/assistant/response_formatter.rb', line 69

def initialize(content, view_context: nil, column_metadata: {})
  @content = content.to_s
  @view_context = view_context
  @column_metadata =  || {}
  @sql_counter = 0
  @product_line_cache = nil # Lazy-loaded lookup hash
end

Instance Method Details

#formatObject

Format the content with all transformations



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'app/services/assistant/response_formatter.rb', line 78

def format
  # Pre-load product lines to avoid N+1 queries
  preload_product_lines

  # Extract ALL fenced code blocks before markdown processing.
  # Each block records its language and raw source for copy/preview buttons.
  content_with_placeholders, code_blocks = extract_code_blocks(@content)

  # Neutralize bracket-style source citations before Kramdown processes them.
  # Kramdown interprets `- [Text]: URL` as a link reference definition and hides the line,
  # leaving empty <li> bullets. Replace `[X]` at the start of list items with `X ยท`.
  content_with_placeholders = neutralize_bracket_sources(content_with_placeholders)

  # Convert markdown to HTML using Kramdown with GFM + Rouge syntax highlighting
  html = render_markdown(content_with_placeholders)

  # Sanitize HTML to prevent XSS from LLM-generated content.
  # LLMs can hallucinate <script> tags or malicious attributes.
  html = sanitize_html(html)

  # Link plain URLs in general response content before code panels are restored.
  # Code blocks are still placeholders at this stage, so URLs in code stay untouched.
  html = auto_link_urls(html)

  # Restore code blocks with rich panels (language header, copy, preview for HTML)
  html = restore_code_blocks(html, code_blocks)

  # Format tables with proper cell formatting
  html = format_tables(html)

  # Link entities to CRM pages
  html = link_entities(html)

  # Style the Sources section with a distinctive panel
  html = format_sources_section(html)

  html.html_safe
end