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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a4dd86789a3ce80b891b88fb9b125bb7b6d85e86928adeb5fd7c1fec671ff56
|
|
4
|
+
data.tar.gz: 171fe031f960d0a6f3abbfacbb20094b35bcd9199cf132e30063868807f95f8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
if
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
data/lib/braintrust/version.rb
CHANGED
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.
|
|
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
|