Class: Assistant::ResponseFormatter
- Inherits:
-
Object
- Object
- Assistant::ResponseFormatter
- 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 columns.
/date|created_at|updated_at|shipped|ordered/i- PERCENTAGE_COLUMNS =
Percentage columns.
/rate|percent|pct|ratio/i- COUNT_COLUMNS =
Count columns.
/count|orders|units|quantity|calls|activities|stock|inventory|qty/i- PRODUCT_LINE_COLUMNS =
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
- GFM_QUIRKS =
GFM parser quirks passed to Kramdown.
:paragraph_endis the parser's own
default (preserved here);:no_auto_typographicdisables the substitution of
--→ en-dash "–",---→ em-dash "—", and...→ ellipsis "…" in running
text. Without it, a double-dash in a query value the AI echoes as a bare URL
(e.g.utm_campaign=spring--sale) is silently rewritten and the link breaks.
Smart quotes (curly ' and ") are a separate option and stay on for prose.
See https://kramdown.gettalong.org/parser/gfm.html %i[paragraph_end no_auto_typographic].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
-
#format ⇒ Object
Format the content with all transformations.
-
#initialize(content, view_context: nil, column_metadata: {}) ⇒ ResponseFormatter
constructor
A new instance of ResponseFormatter.
Constructor Details
#initialize(content, view_context: nil, column_metadata: {}) ⇒ ResponseFormatter
Returns a new instance of ResponseFormatter.
83 84 85 86 87 88 89 |
# File 'app/services/assistant/response_formatter.rb', line 83 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
#format ⇒ Object
Format the content with all transformations
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'app/services/assistant/response_formatter.rb', line 92 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) # Protect supported LaTeX delimiters before Kramdown. Kramdown strips # \(...\) / \[...\] escapes and rewrites $$...$$, so the fragments must # survive Markdown as opaque placeholders. Inline code is excluded. latex_formatter = LatexFragmentFormatter.new(content_with_placeholders) content_with_placeholders = latex_formatter.extract # 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) # Restore safe, explicit hooks for the on-demand KaTeX controller only # after all HTML parsing/linkification is complete. The raw formula stays # visible as a progressive fallback until KaTeX loads. html = latex_formatter.restore(html) html.html_safe end |