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
7 changes: 4 additions & 3 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ OPTIONAL_GEMS = {
"ruby_llm" => {
"1.8" => {constraint: "~> 1.8.0", deps: {}},
"1.9" => {constraint: "~> 1.9.0", deps: {}},
"latest" => {constraint: ">= 1.9", deps: {}}
"2.0" => {constraint: "~> 2.0.0", deps: {}},
"latest" => {constraint: ">= 2.0", deps: {}}
}
}

Expand Down Expand Up @@ -68,7 +69,7 @@ end
appraise "contrib" do
gem "openai", ">= 0.34"
gem "anthropic", ">= 1.11"
gem "ruby_llm", ">= 1.9"
gem "ruby_llm", ">= 2.0"
gem "base64" # needed for openai gem on Ruby 3.4+
gem "cgi" # needed for openai/anthropic gems on Ruby 4.0+
end
Expand All @@ -77,7 +78,7 @@ end
appraise "server" do
gem "openai", ">= 0.34"
gem "anthropic", ">= 1.11"
gem "ruby_llm", ">= 1.9"
gem "ruby_llm", ">= 2.0"
gem "base64" # needed for openai gem on Ruby 3.4+
gem "cgi" # needed for openai/anthropic gems on Ruby 4.0+
gem "rack", "~> 3.0"
Expand Down
2 changes: 1 addition & 1 deletion gemfiles/contrib.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ gem "webmock", "~> 3.0"
gem "yard", "~> 0.9"
gem "openai", ">= 0.34"
gem "anthropic", ">= 1.11"
gem "ruby_llm", ">= 1.9"
gem "ruby_llm", ">= 2.0"
gem "base64"
gem "cgi"

Expand Down
2 changes: 1 addition & 1 deletion gemfiles/ruby_llm.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ gem "standard", "~> 1.0"
gem "vcr", "~> 6.0"
gem "webmock", "~> 3.0"
gem "yard", "~> 0.9"
gem "ruby_llm", ">= 1.9"
gem "ruby_llm", ">= 2.0"

gemspec path: "../"
19 changes: 19 additions & 0 deletions gemfiles/ruby_llm_2_0.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This file was generated by Appraisal

source "https://rubygems.org"

gem "appraisal", "~> 2.5"
gem "climate_control", "~> 1.2"
gem "kramdown", "~> 2.0"
gem "minitest-reporters", "~> 1.6"
gem "minitest-stub-const", "~> 0.6"
gem "minitest", "~> 5.0"
gem "rake", "~> 13.0"
gem "simplecov", "~> 0.22"
gem "standard", "~> 1.0"
gem "vcr", "~> 6.0"
gem "webmock", "~> 3.0"
gem "yard", "~> 0.9"
gem "ruby_llm", "~> 2.0.0"

gemspec path: "../"
2 changes: 1 addition & 1 deletion gemfiles/server.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ gem "webmock", "~> 3.0"
gem "yard", "~> 0.9"
gem "openai", ">= 0.34"
gem "anthropic", ">= 1.11"
gem "ruby_llm", ">= 1.9"
gem "ruby_llm", ">= 2.0"
gem "base64"
gem "cgi"
gem "rack", "~> 3.0"
Expand Down
62 changes: 51 additions & 11 deletions lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,17 +186,17 @@ def format_tool_schema(tool, provider)
::RubyLLM::Providers::OpenAI::Tools.tool_for(tool)
elsif defined?(::RubyLLM::Providers::Anthropic) && provider.is_a?(::RubyLLM::Providers::Anthropic)
::RubyLLM::Providers::Anthropic::Tools.tool_for(tool)
elsif tool.respond_to?(:params_schema) && tool.params_schema
elsif tool_params_schema(tool)
build_basic_tool_schema(tool)
else
build_minimal_tool_schema(tool)
end
rescue NameError, ArgumentError => e
Braintrust::Log.debug("Failed to extract tool schema using provider-specific method: #{e.class.name}: #{e.message}")
tool_schema = (tool.respond_to?(:params_schema) && tool.params_schema) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
end
else
tool_schema = (tool.respond_to?(:params_schema) && tool.params_schema) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
end

# Strip RubyLLM-specific fields to match native OpenAI format
Expand All @@ -217,14 +217,27 @@ def format_tool_schema(tool, provider)
tool_schema
end

# A tool's JSON Schema, across ruby_llm versions.
# 1.x exposes it as params_schema; 2.0 renamed it to parameters_schema.
# @param tool [Object] the RubyLLM tool
# @return [Hash, nil] the schema, or nil when the tool declares none
def tool_params_schema(tool)
%i[params_schema parameters_schema].each do |name|
next unless tool.respond_to?(name)
schema = tool.public_send(name)
return schema if schema
end
nil
end

# Build a basic tool schema with parameters
def build_basic_tool_schema(tool)
{
"type" => "function",
"function" => {
"name" => tool.name.to_s,
"description" => tool.description,
"parameters" => tool.params_schema
"parameters" => tool_params_schema(tool)
}
}
end
Expand Down Expand Up @@ -255,10 +268,11 @@ def format_message_for_input(msg)
# Handle content
if msg.respond_to?(:content) && msg.content
raw_content = msg.content
attachments = extract_attachments(msg)

# Check if content is a Content object with attachments (issue #71)
formatted["content"] = if raw_content.respond_to?(:text) && raw_content.respond_to?(:attachments) && raw_content.attachments&.any?
format_multipart_content(raw_content)
# Include attachments alongside the text when present (issue #71)
formatted["content"] = if attachments.any?
format_multipart_content(content_text(raw_content), attachments)
else
format_simple_content(raw_content, msg.role.to_s)
end
Expand All @@ -278,17 +292,43 @@ def format_message_for_input(msg)
formatted
end

# Collect a message's attachments.
#
# ruby_llm 1.x wraps text and attachments in a Content object hanging off
# the message; 2.0 removed Content, leaving content a plain String and
# exposing attachments on the message itself. Support both.
#
# @param msg [Object] the RubyLLM message
# @return [Array] the message's attachments, empty when there are none
def extract_attachments(msg)
content = msg.content if msg.respond_to?(:content)
if content.respond_to?(:attachments)
from_content = Array(content.attachments)
return from_content if from_content.any?
end

msg.respond_to?(:attachments) ? Array(msg.attachments) : []
end

# Extract the plain text of a message's content.
# @param raw_content [Object] String, or a 1.x Content object
# @return [Object] the text
def content_text(raw_content)
raw_content.respond_to?(:text) ? raw_content.text : raw_content
end

# Format multipart content with text and attachments
# @param content_obj [Object] Content object with text and attachments
# @param text [Object] the message text
# @param attachments [Array] the message's attachments
# @return [Array<Hash>] array of content parts
def format_multipart_content(content_obj)
def format_multipart_content(text, attachments)
content_parts = []

# Add text part
content_parts << {"type" => "text", "text" => content_obj.text} if content_obj.text
content_parts << {"type" => "text", "text" => text} if text

# Add attachment parts (convert to Braintrust format)
content_obj.attachments.each do |attachment|
attachments.each do |attachment|
content_parts << format_attachment_for_input(attachment)
end

Expand Down
9 changes: 5 additions & 4 deletions test/braintrust/contrib/ruby_llm/deprecated_test.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

require "test_helper"
require_relative "integration_helper"
require "braintrust/contrib/ruby_llm/deprecated"

class Braintrust::Contrib::RubyLLM::DeprecatedTest < Minitest::Test
include Braintrust::Contrib::RubyLLM::IntegrationHelper

# --- .wrap ---

def test_wrap_delegates_to_instrument
Expand Down Expand Up @@ -105,12 +108,10 @@ def test_unwrap_returns_chat
def test_unwrap_disables_instrumentation_for_subsequent_requests
skip "RubyLLM gem not available" unless defined?(::RubyLLM::Chat)

VCR.use_cassette("contrib/ruby_llm/basic_chat") do
VCR.use_cassette(ruby_llm_cassette("basic_chat")) do
rig = setup_otel_test_rig

RubyLLM.configure do |config|
config.openai_api_key = get_openai_key
end
configure_ruby_llm_for_vcr

chat = RubyLLM.chat(model: "gpt-4o-mini")

Expand Down
Loading
Loading