braintrust 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 990aebc768cbd09ff44934e1c69c355d08b0c2f6f59da43de57a822ab4271aad
4
- data.tar.gz: 4e99ad835dee1a5c45a9244cefca6478eab5ecb5a4f4e3c7ca929c3c94e3672f
3
+ metadata.gz: 4a4dd86789a3ce80b891b88fb9b125bb7b6d85e86928adeb5fd7c1fec671ff56
4
+ data.tar.gz: 171fe031f960d0a6f3abbfacbb20094b35bcd9199cf132e30063868807f95f8b
5
5
  SHA512:
6
- metadata.gz: 4e7646252645b7fb6e5b220a0fdfda171af86e6f12cec2eb0d31b261d764183d55aaf931100c59f2ad09ff75723f9cf3882835874148591d0ce301b464d594b4
7
- data.tar.gz: 0f81b3ef9a3032d785af30612c05d38c645b56cf8227745bc8bb06e1d07a0d330c1483b2e4af628e43ef98078ea854f9baab0d76dfa9e485586bda6d38fdd549
6
+ metadata.gz: 3cf52e457ca5d4cdad2f8873bb45d0eeb0b79b33c5edbcd467e65ce7261caea9bcc86ea9d562835197279105f14f2787bcac6622faad4ec420a5f2fa27a003d2
7
+ data.tar.gz: 5a7ccc20a3e63840e15c41dd82eaa8653a2f0dce693321e6527ace59c54255524ae1d794d2dce058127b747a514e8777729384cde9c157374039880bb3b01013
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Braintrust
4
+ module Internal
5
+ # Encoding utilities using Ruby's native pack/unpack methods.
6
+ # Avoids dependency on external gems that became bundled gems in Ruby 3.4.
7
+ module Encoding
8
+ # Base64 encoding/decoding using Ruby's native pack/unpack methods.
9
+ # Drop-in replacement for the base64 gem's strict methods.
10
+ #
11
+ # @example Encode binary data
12
+ # Encoding::Base64.strict_encode64(image_bytes)
13
+ # # => "iVBORw0KGgo..."
14
+ #
15
+ # @example Decode base64 string
16
+ # Encoding::Base64.strict_decode64("iVBORw0KGgo...")
17
+ # # => "\x89PNG..."
18
+ #
19
+ module Base64
20
+ module_function
21
+
22
+ # Encodes binary data to base64 without newlines (strict encoding).
23
+ #
24
+ # @param data [String] Binary data to encode
25
+ # @return [String] Base64-encoded string without newlines
26
+ def strict_encode64(data)
27
+ [data].pack("m0")
28
+ end
29
+
30
+ # Decodes a base64 string to binary data (strict decoding).
31
+ #
32
+ # @param str [String] Base64-encoded string
33
+ # @return [String] Decoded binary data
34
+ def strict_decode64(str)
35
+ str.unpack1("m0")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
3
  require "net/http"
4
+ require_relative "../internal/encoding"
5
5
  require "uri"
6
6
 
7
7
  module Braintrust
@@ -110,7 +110,7 @@ module Braintrust
110
110
  # att.to_data_url
111
111
  # # => "data:image/png;base64,iVBORw0KGgo..."
112
112
  def to_data_url
113
- encoded = Base64.strict_encode64(@data)
113
+ encoded = Internal::Encoding::Base64.strict_encode64(@data)
114
114
  "data:#{@content_type};base64,#{encoded}"
115
115
  end
116
116
 
@@ -4,6 +4,7 @@ require "opentelemetry/sdk"
4
4
  require "json"
5
5
  require_relative "../../../tokens"
6
6
  require_relative "../../../../logger"
7
+ require_relative "../../../../internal/encoding"
7
8
 
8
9
  module Braintrust
9
10
  module Trace
@@ -422,18 +423,14 @@ module Braintrust
422
423
 
423
424
  # Handle content
424
425
  if msg.respond_to?(:content) && msg.content
425
- # Convert Ruby hash notation to JSON string for tool results
426
- content = msg.content
427
- if msg.role.to_s == "tool" && content.is_a?(String) && content.start_with?("{:")
428
- # Ruby hash string like "{:location=>...}" - try to parse and re-serialize as JSON
429
- begin
430
- # Simple conversion: replace Ruby hash syntax with JSON
431
- content = content.gsub(/(?<=\{|, ):(\w+)=>/, '"\1":').gsub("=>", ":")
432
- rescue
433
- # Keep original if conversion fails
434
- end
426
+ raw_content = msg.content
427
+
428
+ # Check if content is a Content object with attachments (issue #71)
429
+ formatted["content"] = if raw_content.respond_to?(:text) && raw_content.respond_to?(:attachments) && raw_content.attachments&.any?
430
+ format_multipart_content(raw_content)
431
+ else
432
+ format_simple_content(raw_content, msg.role.to_s)
435
433
  end
436
- formatted["content"] = content
437
434
  end
438
435
 
439
436
  # Handle tool_calls for assistant messages
@@ -450,6 +447,74 @@ module Braintrust
450
447
  formatted
451
448
  end
452
449
 
450
+ # Format multipart content with text and attachments
451
+ # @param content_obj [Object] Content object with text and attachments
452
+ # @return [Array<Hash>] array of content parts
453
+ def self.format_multipart_content(content_obj)
454
+ content_parts = []
455
+
456
+ # Add text part
457
+ content_parts << {"type" => "text", "text" => content_obj.text} if content_obj.text
458
+
459
+ # Add attachment parts (convert to Braintrust format)
460
+ content_obj.attachments.each do |attachment|
461
+ content_parts << format_attachment_for_input(attachment)
462
+ end
463
+
464
+ content_parts
465
+ end
466
+
467
+ # Format simple text content
468
+ # @param raw_content [Object] String or Content object with text
469
+ # @param role [String] the message role
470
+ # @return [String] formatted text content
471
+ def self.format_simple_content(raw_content, role)
472
+ content = raw_content
473
+ content = content.text if content.respond_to?(:text)
474
+
475
+ # Convert Ruby hash string to JSON for tool results
476
+ if role == "tool" && content.is_a?(String) && content.start_with?("{:")
477
+ begin
478
+ content = content.gsub(/(?<=\{|, ):(\w+)=>/, '"\1":').gsub("=>", ":")
479
+ rescue
480
+ # Keep original if conversion fails
481
+ end
482
+ end
483
+
484
+ content
485
+ end
486
+
487
+ # Format a RubyLLM attachment to OpenAI-compatible format
488
+ # @param attachment [Object] the RubyLLM attachment
489
+ # @return [Hash] OpenAI image_url format for consistency with other integrations
490
+ def self.format_attachment_for_input(attachment)
491
+ # RubyLLM Attachment has: source (Pathname), filename, mime_type
492
+ if attachment.respond_to?(:source) && attachment.source
493
+ begin
494
+ data = File.binread(attachment.source.to_s)
495
+ encoded = Internal::Encoding::Base64.strict_encode64(data)
496
+ mime_type = attachment.respond_to?(:mime_type) ? attachment.mime_type : "application/octet-stream"
497
+
498
+ # Use OpenAI's image_url format for consistency
499
+ {
500
+ "type" => "image_url",
501
+ "image_url" => {
502
+ "url" => "data:#{mime_type};base64,#{encoded}"
503
+ }
504
+ }
505
+ rescue => e
506
+ Log.debug("Failed to read attachment file: #{e.message}")
507
+ # Return a placeholder if we can't read the file
508
+ {"type" => "text", "text" => "[attachment: #{attachment.respond_to?(:filename) ? attachment.filename : "unknown"}]"}
509
+ end
510
+ elsif attachment.respond_to?(:to_h)
511
+ # Try to use attachment's own serialization
512
+ attachment.to_h
513
+ else
514
+ {"type" => "text", "text" => "[attachment]"}
515
+ end
516
+ end
517
+
453
518
  # Capture streaming output and metrics
454
519
  # @param span [OpenTelemetry::Trace::Span] the span
455
520
  # @param aggregated_chunks [Array] the aggregated chunks
@@ -458,8 +523,11 @@ module Braintrust
458
523
  return if aggregated_chunks.empty?
459
524
 
460
525
  # Aggregate content from chunks
526
+ # Extract text from Content objects if present (issue #71)
461
527
  aggregated_content = aggregated_chunks.map { |c|
462
- c.respond_to?(:content) ? c.content : c.to_s
528
+ content = c.respond_to?(:content) ? c.content : c.to_s
529
+ content = content.text if content.respond_to?(:text)
530
+ content
463
531
  }.join
464
532
 
465
533
  output = [{
@@ -490,8 +558,11 @@ module Braintrust
490
558
  }
491
559
 
492
560
  # Add content if it's a simple text response
561
+ # Extract text from Content objects if present (issue #71)
493
562
  if response.respond_to?(:content) && response.content && !response.content.empty?
494
- message["content"] = response.content
563
+ content = response.content
564
+ content = content.text if content.respond_to?(:text)
565
+ message["content"] = content
495
566
  end
496
567
 
497
568
  # Check if there are tool calls in the messages history
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Braintrust
4
- VERSION = "0.0.11"
4
+ VERSION = "0.0.12"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: braintrust
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Braintrust
@@ -201,6 +201,7 @@ files:
201
201
  - lib/braintrust/eval/runner.rb
202
202
  - lib/braintrust/eval/scorer.rb
203
203
  - lib/braintrust/eval/summary.rb
204
+ - lib/braintrust/internal/encoding.rb
204
205
  - lib/braintrust/internal/experiments.rb
205
206
  - lib/braintrust/internal/thread_pool.rb
206
207
  - lib/braintrust/logger.rb