Module: Braintrust::Eval::Formatter
- Defined in:
- lib/braintrust/eval/formatter.rb
Overview
Formatter for pretty CLI output of experiment results Uses ANSI colors and Unicode box drawing for terminal display
Constant Summary collapse
- COLORS =
ANSI color codes
{ gray: "\e[90m", red: "\e[31m", green: "\e[32m", blue: "\e[34m", magenta: "\e[35m", white: "\e[97m", dim: "\e[2m", reset: "\e[0m" }.freeze
- BOX =
Box drawing characters (Unicode)
{ top_left: "╭", top_right: "╮", bottom_left: "╰", bottom_right: "╯", horizontal: "─", vertical: "│" }.freeze
- MAX_ERROR_LENGTH =
Maximum length for error messages before truncation
150
Class Method Summary collapse
-
.colorize(text, *styles) ⇒ String
Apply ANSI color codes to text.
-
.format_duration(duration) ⇒ String
Format duration for display.
-
.format_error_row(error_message) ⇒ String
Format an error row for display.
-
.format_experiment_summary(summary) ⇒ String
Format an experiment summary for CLI output.
-
.format_metadata_row(label, value) ⇒ String
Format a metadata row (label: value).
-
.format_score_row(score, name_width = 20) ⇒ String
Format a score row for display.
-
.pad_cell(text, width, align) ⇒ String
Pad a cell to a given width, accounting for ANSI codes.
-
.terminal_link(text, url) ⇒ String
Create a clickable terminal hyperlink (OSC 8).
-
.truncate_error(message, max_length) ⇒ String
Truncate error message to max length with ellipsis.
-
.visible_text_length(text) ⇒ Integer
Calculate visible text length, stripping ANSI codes and OSC 8 hyperlinks.
-
.wrap_in_box(lines, title) ⇒ String
Wrap content lines in a Unicode box with title.
Class Method Details
.colorize(text, *styles) ⇒ String
Apply ANSI color codes to text
122 123 124 125 126 |
# File 'lib/braintrust/eval/formatter.rb', line 122 def colorize(text, *styles) return text unless $stdout.tty? codes = styles.map { |s| COLORS[s] }.compact.join "#{codes}#{text}#{COLORS[:reset]}" end |
.format_duration(duration) ⇒ String
Format duration for display
93 94 95 96 97 98 99 |
# File 'lib/braintrust/eval/formatter.rb', line 93 def format_duration(duration) if duration < 1 "#{(duration * 1000).round(0)}ms" else "#{duration.round(4)}s" end end |
.format_error_row(error_message) ⇒ String
Format an error row for display
104 105 106 107 |
# File 'lib/braintrust/eval/formatter.rb', line 104 def format_error_row() truncated = truncate_error(, MAX_ERROR_LENGTH) "#{colorize("✗", :red)} #{truncated}" end |
.format_experiment_summary(summary) ⇒ String
Format an experiment summary for CLI output
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/braintrust/eval/formatter.rb', line 37 def format_experiment_summary(summary) return "" unless summary lines = [] # Metadata section lines << ("Project", summary.project_name) lines << ("Experiment", summary.experiment_name) lines << ("ID", summary.experiment_id) lines << ("Duration", format_duration(summary.duration)) lines << ("Errors", summary.error_count.to_s) # Scores section (if any) if summary.scores&.any? lines << "" lines << colorize("Scores", :white) # Calculate max scorer name length for alignment max_name_len = summary.scores.values.map { |s| s.name.length }.max || 0 name_width = [max_name_len + 2, 20].max # +2 for "◯ " prefix summary.scores.each_value do |score| lines << format_score_row(score, name_width) end end # Errors section (if any) if summary.errors&.any? lines << "" lines << colorize("Errors", :white) summary.errors.each do |error| lines << format_error_row(error) end end # Footer link if summary.experiment_url lines << "" lines << terminal_link("View results for #{summary.experiment_name}", summary.experiment_url) end wrap_in_box(lines, "Experiment summary") end |
.format_metadata_row(label, value) ⇒ String
Format a metadata row (label: value)
86 87 88 |
# File 'lib/braintrust/eval/formatter.rb', line 86 def (label, value) "#{colorize(label + ":", :dim)} #{value}" end |
.format_score_row(score, name_width = 20) ⇒ String
Format a score row for display
132 133 134 135 136 |
# File 'lib/braintrust/eval/formatter.rb', line 132 def format_score_row(score, name_width = 20) name = "#{colorize("◯", :blue)} #{score.name}" value = colorize("#{(score.score_mean * 100).round(2)}%", :white) pad_cell(name, name_width, :left) + " " + pad_cell(value, 10, :right) end |
.pad_cell(text, width, align) ⇒ String
Pad a cell to a given width, accounting for ANSI codes
143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/braintrust/eval/formatter.rb', line 143 def pad_cell(text, width, align) visible_length = visible_text_length(text) padding = [width - visible_length, 0].max case align when :right " " * padding + text else text + " " * padding end end |
.terminal_link(text, url) ⇒ String
Create a clickable terminal hyperlink (OSC 8)
171 172 173 174 175 176 177 |
# File 'lib/braintrust/eval/formatter.rb', line 171 def terminal_link(text, url) if $stdout.tty? "\e]8;;#{url}\e\\#{text}\e]8;;\e\\" else "#{text}: #{url}" end end |
.truncate_error(message, max_length) ⇒ String
Truncate error message to max length with ellipsis
113 114 115 116 |
# File 'lib/braintrust/eval/formatter.rb', line 113 def truncate_error(, max_length) return if .length <= max_length "#{[0, max_length - 3]}..." end |
.visible_text_length(text) ⇒ Integer
Calculate visible text length, stripping ANSI codes and OSC 8 hyperlinks
158 159 160 161 162 163 164 165 |
# File 'lib/braintrust/eval/formatter.rb', line 158 def visible_text_length(text) # Strip ANSI color codes: \e[...m # Strip OSC 8 hyperlinks: \e]8;;...\e\\ (the URL part is invisible) text .gsub(/\e\[[0-9;]*m/, "") # ANSI color codes .gsub(/\e\]8;;[^\e]*\e\\/, "") # OSC 8 hyperlink sequences .length end |
.wrap_in_box(lines, title) ⇒ String
Wrap content lines in a Unicode box with title
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/braintrust/eval/formatter.rb', line 183 def wrap_in_box(lines, title) # Calculate width from content (strip escape sequences for measurement) content_width = lines.map { |l| visible_text_length(l) }.max || 0 box_width = [content_width + 4, title.length + 6].max inner_width = box_width - 2 result = [] # Top border with title title_str = " #{title} " remaining = inner_width - title_str.length - 1 top = colorize("#{BOX[:top_left]}#{BOX[:horizontal]}", :gray) + colorize(title_str, :gray) + colorize(BOX[:horizontal] * remaining + BOX[:top_right], :gray) result << top # Empty line for padding result << colorize(BOX[:vertical], :gray) + " " * inner_width + colorize(BOX[:vertical], :gray) # Content lines lines.each do |line| visible_len = visible_text_length(line) padding = inner_width - visible_len - 2 # 1 space on each side result << colorize(BOX[:vertical], :gray) + " " + line + " " * [padding, 0].max + " " + colorize(BOX[:vertical], :gray) end # Empty line for padding result << colorize(BOX[:vertical], :gray) + " " * inner_width + colorize(BOX[:vertical], :gray) # Bottom border result << colorize("#{BOX[:bottom_left]}#{BOX[:horizontal] * inner_width}#{BOX[:bottom_right]}", :gray) "\n" + result.join("\n") end |