Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 8 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,46 +46,24 @@ export BRAINTRUST_API_KEY="your-api-key"
```ruby
require "braintrust"

# Initialize Braintrust
Braintrust.init

# Simple food classifier (the code being evaluated)
def classify_food(input)
fruit = %w[apple banana strawberry orange grape mango]
vegetable = %w[carrot broccoli spinach potato tomato cucumber]

input_lower = input.downcase
return "fruit" if fruit.any? { |f| input_lower.include?(f) }
return "vegetable" if vegetable.any? { |v| input_lower.include?(v) }
"unknown"
end
# Define task to evaluate
task = ->(input) { input.include?("a") ? "fruit" : "vegetable" }

# Run an evaluation
result = Braintrust::Eval.run(
# Run evaluation
Braintrust::Eval.run(
project: "my-project",
experiment: "food-classifier-eval",

# Test cases
experiment: "food-classifier",
cases: [
{input: "apple", expected: "fruit"},
{input: "carrot", expected: "vegetable"},
{input: "banana", expected: "fruit"},
{input: "broccoli", expected: "vegetable"}
{input: "carrot", expected: "vegetable"}
],

# Task to evaluate
task: ->(input) { classify_food(input) },

# Scorers to judge output quality
task: task,
scorers: [
Braintrust::Eval.scorer("exact_match") { |input, expected, output|
(output == expected) ? 1.0 : 0.0
}
->(input, expected, output) { output == expected ? 1.0 : 0.0 }
]
)

# View results
puts "View results at: #{result.permalink}"
```

### Tracing
Expand Down Expand Up @@ -128,20 +106,15 @@ puts "View trace in Braintrust!"
require "braintrust"
require "openai"

# Initialize Braintrust
Braintrust.init

# Create OpenAI client
client = OpenAI::Client.new(api_key: ENV["OPENAI_API_KEY"])

# Wrap the client with Braintrust tracing
Braintrust::Trace::OpenAI.wrap(client)

# Create a root span to capture the operation
tracer = OpenTelemetry.tracer_provider.tracer("openai-app")
root_span = nil

# Make a chat completion request (automatically traced!)
response = tracer.in_span("chat-completion") do |span|
root_span = span

Expand All @@ -157,10 +130,8 @@ end

puts "Response: #{response.choices[0].message.content}"

# View the trace
puts "View trace at: #{Braintrust::Trace.permalink(root_span)}"

# Shutdown to flush spans
OpenTelemetry.tracer_provider.shutdown
```

Expand Down
28 changes: 24 additions & 4 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ task :clean do
FileUtils.rm_f("changelog.md")
end

def run_example(example)
prefix = case example
when /openai/, /kitchen-sink/
"bundle exec appraisal openai-latest"
else
"bundle exec"
end

sh "#{prefix} ruby #{example}"
end

desc "Run a single example with the correct gemfile"
task :example, [:path] do |t, args|
example = args[:path]
raise "Usage: rake example[path/to/example.rb]" unless example

puts "Running #{example}..."
run_example(example)
end

desc "Run all examples"
task :examples do
examples = FileList["examples/**/*.rb"].exclude("examples/**/README.md")
Expand All @@ -36,10 +56,7 @@ task :examples do

examples.each do |example|
puts "\n=== Running #{example} ==="
sh "bundle exec ruby #{example}" do |ok, res|
puts "✓ #{example} completed" if ok
puts "✗ #{example} failed (#{res.exitstatus})" unless ok
end
run_example(example)
end
end

Expand Down Expand Up @@ -94,6 +111,9 @@ namespace :test do
sh "bundle exec appraisal rake test"
end

desc "Run tests against all dependency scenarios (alias for test:appraisal)"
task all: :appraisal

namespace :appraisal do
desc "Show help for appraisal scenarios and usage"
task :help do
Expand Down
42 changes: 3 additions & 39 deletions examples/eval.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,7 @@
# Usage:
# bundle exec ruby examples/eval.rb

# Initialize Braintrust with blocking login
Braintrust.init(blocking_login: true)

# Create OpenTelemetry TracerProvider
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new

# Enable Braintrust tracing
Braintrust::Trace.enable(tracer_provider)

# Set as global provider
OpenTelemetry.tracer_provider = tracer_provider
Braintrust.init

# Simple food classifier (the code being evaluated)
# In a real scenario, this would call your model/API
Expand Down Expand Up @@ -71,8 +61,7 @@ def call(input, expected, output, metadata = {})
}

# Run the evaluation
puts "\nRunning evaluation..."
result = Braintrust::Eval.run(
Braintrust::Eval.run(
# Required: Project and experiment
project: "ruby-sdk-examples",
experiment: "food-classifier-eval",
Expand Down Expand Up @@ -130,30 +119,5 @@ def call(input, expected, output, metadata = {})
}
)

# Inspect the results
puts "\n" + "=" * 50
puts "Evaluation Complete!"
puts "=" * 50

puts "\nExperiment: #{result.experiment_name}"
puts "Project ID: #{result.project_id}"
puts "Duration: #{result.duration.round(2)}s"
puts "Status: #{result.success? ? "✓ Success" : "✗ Failed"}"

# Show the permalink to view in Braintrust UI
puts "\nView results at:"
puts " #{result.permalink}"

# Show errors if any
if result.failed?
puts "\nErrors (#{result.errors.length}):"
result.errors.each do |error|
puts " - #{error}"
end
exit 1
end

puts "\n✓ All test cases passed!"

# Shutdown to flush spans to Braintrust
tracer_provider.shutdown
OpenTelemetry.tracer_provider.shutdown
11 changes: 2 additions & 9 deletions examples/eval/dataset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@
require "bundler/setup"
require "braintrust"

# Initialize Braintrust with login (sets global state)
Braintrust.init(blocking_login: true)
Braintrust.init
api = Braintrust::API.new # Uses global state

# Enable tracing to send spans to Braintrust
require "opentelemetry/sdk"
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new
Braintrust::Trace.enable(tracer_provider)
OpenTelemetry.tracer_provider = tracer_provider
at_exit { tracer_provider.shutdown }
at_exit { OpenTelemetry.tracer_provider.shutdown }

# Project name
project_name = "ruby-sdk-examples"
Expand Down
14 changes: 2 additions & 12 deletions examples/internal/evals-with-errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,7 @@
# Usage:
# bundle exec ruby examples/internal/evals-with-errors.rb

# Initialize Braintrust with blocking login
Braintrust.init(blocking_login: true)

# Create OpenTelemetry TracerProvider
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new

# Enable Braintrust tracing
Braintrust::Trace.enable(tracer_provider)

# Set as global provider
OpenTelemetry.tracer_provider = tracer_provider
Braintrust.init

puts "Evals with Errors Example"
puts "=" * 60
Expand Down Expand Up @@ -217,4 +207,4 @@ def risky_task(input)
end

# Shutdown to flush spans to Braintrust
tracer_provider.shutdown
OpenTelemetry.tracer_provider.shutdown
22 changes: 6 additions & 16 deletions examples/internal/kitchen-sink.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,13 @@
exit 1
end

# Initialize Braintrust with blocking login
Braintrust.init(blocking_login: true)

# Create OpenTelemetry TracerProvider
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new

# Enable Braintrust tracing
Braintrust::Trace.enable(tracer_provider)

# Set as global provider
OpenTelemetry.tracer_provider = tracer_provider
Braintrust.init

# Create OpenAI client
openai_client = OpenAI::Client.new(api_key: ENV["OPENAI_API_KEY"])

# Wrap the client with Braintrust tracing
Braintrust::Trace::OpenAI.wrap(openai_client, tracer_provider: tracer_provider)
Braintrust::Trace::OpenAI.wrap(openai_client)

puts "Kitchen Sink Eval Example"
puts "=" * 60
Expand Down Expand Up @@ -363,10 +353,10 @@ def call(input, expected, output, metadata = {})
result.errors.each_with_index do |error, i|
puts " #{i + 1}. #{error}"
end
exit 1
puts "\nNote: Some errors are intentional to demonstrate error handling."
else
puts "\n✓ All test cases completed successfully!"
end

puts "\n✓ All test cases completed successfully!"

# Shutdown to flush spans
tracer_provider.shutdown
OpenTelemetry.tracer_provider.shutdown
77 changes: 15 additions & 62 deletions lib/braintrust.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# @example Initialize with global state
# Braintrust.init(
# api_key: ENV['BRAINTRUST_API_KEY'],
# project: "my-project"
# default_project: "my-project"
# )
#
# @example Initialize with explicit state
Expand All @@ -27,42 +27,31 @@ module Braintrust
class Error < StandardError; end

# Initialize Braintrust SDK
# Creates a State from config (ENV + options) and optionally sets it as global
#
# By default, kicks off an async background login that retries indefinitely.
# Use blocking_login: true to login synchronously before returning.
#
# @param set_global [Boolean] whether to set as global state (default: true)
# @param blocking_login [Boolean] whether to block and login synchronously (default: false, which starts async login)
# @param tracing [Boolean] whether to enable OpenTelemetry tracing (default: true)
# @param tracer_provider [TracerProvider, nil] Optional tracer provider to use instead of creating one
# @param api_key [String, nil] Braintrust API key (overrides BRAINTRUST_API_KEY env var)
# @param org_name [String, nil] Organization name (overrides BRAINTRUST_ORG_NAME env var)
# @param default_project [String, nil] Default project for spans (overrides BRAINTRUST_DEFAULT_PROJECT env var, format: "project_name:my-project" or "project_id:uuid")
# @param app_url [String, nil] App URL (overrides BRAINTRUST_APP_URL env var, default: https://www.braintrust.dev)
# @param api_url [String, nil] API URL (overrides BRAINTRUST_API_URL env var, default: https://api.braintrust.dev)
# @param set_global [Boolean] Whether to set as global state (default: true)
# @param blocking_login [Boolean] Whether to block and login synchronously (default: false - async background login)
# @param enable_tracing [Boolean] Whether to enable OpenTelemetry tracing (default: true)
# @param tracer_provider [TracerProvider, nil] Optional tracer provider to use instead of creating one
# @return [State] the created state
def self.init(set_global: true, blocking_login: false, tracing: true, tracer_provider: nil, **options)
config = Config.from_env(**options)
state = State.new(
api_key: config.api_key,
org_name: config.org_name,
default_project: config.default_project,
app_url: config.app_url,
api_url: config.api_url
def self.init(api_key: nil, org_name: nil, default_project: nil, app_url: nil, api_url: nil, set_global: true, blocking_login: false, enable_tracing: true, tracer_provider: nil)
state = State.from_env(
api_key: api_key,
org_name: org_name,
default_project: default_project,
app_url: app_url,
api_url: api_url,
blocking_login: blocking_login,
enable_tracing: enable_tracing,
tracer_provider: tracer_provider
)

State.global = state if set_global

# Login: either blocking (synchronous) or async (background thread with retries)
if blocking_login
state.login
else
state.login_in_thread # Default: async background login
end

setup_tracing(state, tracer_provider) if tracing

state
end

Expand All @@ -71,40 +60,4 @@ def self.init(set_global: true, blocking_login: false, tracing: true, tracer_pro
def self.current_state
State.global
end

class << self
private

# Set up OpenTelemetry tracing with Braintrust
# @param state [State] Braintrust state
# @param explicit_provider [TracerProvider, nil] Optional explicit tracer provider
# @return [void]
def setup_tracing(state, explicit_provider = nil)
require "opentelemetry/sdk"

if explicit_provider
# Use the explicitly provided tracer provider
# DO NOT set as global - user is managing it themselves
Log.debug("Using explicitly provided OpenTelemetry tracer provider")
tracer_provider = explicit_provider
else
# Check if global tracer provider is already a real TracerProvider
current_provider = OpenTelemetry.tracer_provider

if current_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
# Use existing provider
Log.debug("Using existing OpenTelemetry tracer provider")
tracer_provider = current_provider
else
# Create new provider and set as global
tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new
OpenTelemetry.tracer_provider = tracer_provider
Log.debug("Created OpenTelemetry tracer provider")
end
end

# Enable Braintrust tracing (adds span processor)
Trace.enable(tracer_provider, state: state)
end
end
end
Loading