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

Class Method Details

.colorize(text, *styles) ⇒ String

Apply ANSI color codes to text

Parameters:

  • text (String)

    Text to colorize

  • styles (Array<Symbol>)

    Color/style names (:gray, :red, :green, etc.)

Returns:

  • (String)

    Colorized text (or plain text if not a TTY)



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

Parameters:

  • duration (Float)

    Duration in seconds

Returns:

  • (String)

    Formatted duration (e.g., “1.2345s” or “123ms”)



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

Parameters:

  • error_message (String)

    The error message

Returns:

  • (String)

    Formatted row with red ✗



104
105
106
107
# File 'lib/braintrust/eval/formatter.rb', line 104

def format_error_row(error_message)
  truncated = truncate_error(error_message, MAX_ERROR_LENGTH)
  "#{colorize("", :red)} #{truncated}"
end

.format_experiment_summary(summary) ⇒ String

Format an experiment summary for CLI output

Parameters:

Returns:

  • (String)

    Formatted output with box drawing and colors



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)

Parameters:

  • label (String)

    Row label

  • value (String)

    Row value

Returns:

  • (String)

    Formatted row



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

Parameters:

  • score (ScorerStats)

    The scorer statistics

  • name_width (Integer) (defaults to: 20)

    Width for the name column

Returns:

  • (String)

    Formatted row



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

Parameters:

  • text (String)

    Cell text (may contain ANSI codes)

  • width (Integer)

    Target width

  • align (Symbol)

    :left or :right alignment

Returns:

  • (String)

    Padded cell



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

Create a clickable terminal hyperlink (OSC 8)

Parameters:

  • text (String)

    Display text

  • url (String)

    Target URL

Returns:

  • (String)

    Hyperlinked text (or plain text with URL if not a TTY)



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

Parameters:

  • message (String)

    The error message

  • max_length (Integer)

    Maximum length before truncation

Returns:

  • (String)

    Truncated message



113
114
115
116
# File 'lib/braintrust/eval/formatter.rb', line 113

def truncate_error(message, max_length)
  return message if message.length <= max_length
  "#{message[0, max_length - 3]}..."
end

.visible_text_length(text) ⇒ Integer

Calculate visible text length, stripping ANSI codes and OSC 8 hyperlinks

Parameters:

  • text (String)

    Text that may contain escape sequences

Returns:

  • (Integer)

    Visible character count



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

Parameters:

  • lines (Array<String>)

    Content lines

  • title (String)

    Box title

Returns:

  • (String)

    Boxed content



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