braintrust 0.1.3 → 0.1.4

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: 83ff9b69dc144333dba85a5f68e5a40d482ca99b3cae4bb55abe24ef2d05c296
4
- data.tar.gz: 1a52913de27b3536c7881203f91d3d6050d53e66afeb90900d3c8a04b180951d
3
+ metadata.gz: e1a5c8840f707c7b4da95e4ccc8abea32591606d667a309432f2955d5df26eca
4
+ data.tar.gz: a45e62f34a1d59dd11e1cc46ff8d128a495a45a80fa1ce2026c76b648b58de89
5
5
  SHA512:
6
- metadata.gz: fb7da28ba278c6a1cff5bd143e28808c723f1bf1507a6fe73d55b76f81d17e74ffc62f5c5dde030a1a5101797f3399592d13d525d46ad39b6b96047f7e47a3d6
7
- data.tar.gz: d49db21d70faba9e3e9b59a61b88d55897cd5420ef636ae18c7b68a4c50d1428be2fbe0c0efc0b26f3a159bdbfb629025f0cc1aa8489187ecf5b586d57c8e1d2
6
+ metadata.gz: 75b71465a80ed2cfd3c6600113dd62357d01e0bd672f2043045f56d7d0223882cc2c5fd9f8927973ae546f99b68971bbe66fd34d66ec6fd62fafd65ca52abcd7
7
+ data.tar.gz: 06eb21fec07c05755aacd0a214cd16594f47a7033e723a2798df26692a0c15ccd9d5ed614588cc805620d303bb0f1a1c577c66b25c92c6dbbaa40283023cd662
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
6
  require_relative "../logger"
7
+ require_relative "../internal/http"
7
8
 
8
9
  module Braintrust
9
10
  class API
@@ -111,6 +112,7 @@ module Braintrust
111
112
  payload[:version] = version if version
112
113
 
113
114
  response = http_post_json_raw("/btql", payload)
115
+ Braintrust::Internal::Http.decompress_response!(response)
114
116
 
115
117
  # Parse JSONL response
116
118
  records = response.body.lines
@@ -158,9 +160,7 @@ module Braintrust
158
160
  start_time = Time.now
159
161
  Log.debug("[API] #{method.upcase} #{uri}")
160
162
 
161
- http = Net::HTTP.new(uri.host, uri.port)
162
- http.use_ssl = (uri.scheme == "https")
163
- response = http.request(request)
163
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
164
164
 
165
165
  duration_ms = ((Time.now - start_time) * 1000).round(2)
166
166
  Log.debug("[API] #{method.upcase} #{uri} -> #{response.code} (#{duration_ms}ms, #{response.body.bytesize} bytes)")
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
6
  require_relative "../logger"
7
+ require_relative "../internal/http"
7
8
 
8
9
  module Braintrust
9
10
  class API
@@ -242,9 +243,7 @@ module Braintrust
242
243
  start_time = Time.now
243
244
  Log.debug("[API] #{method.upcase} #{uri}")
244
245
 
245
- http = Net::HTTP.new(uri.host, uri.port)
246
- http.use_ssl = (uri.scheme == "https")
247
- response = http.request(request)
246
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
248
247
 
249
248
  duration_ms = ((Time.now - start_time) * 1000).round(2)
250
249
  Log.debug("[API] #{method.upcase} #{uri} -> #{response.code} (#{duration_ms}ms, #{response.body.bytesize} bytes)")
@@ -4,6 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
6
  require_relative "../../logger"
7
+ require_relative "../../internal/http"
7
8
 
8
9
  module Braintrust
9
10
  class API
@@ -44,12 +45,7 @@ module Braintrust
44
45
  request = Net::HTTP::Post.new(uri)
45
46
  request["Authorization"] = "Bearer #{api_key}"
46
47
 
47
- http = Net::HTTP.new(uri.hostname, uri.port)
48
- http.use_ssl = true if uri.scheme == "https"
49
-
50
- response = http.start do |http_session|
51
- http_session.request(request)
52
- end
48
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
53
49
 
54
50
  Log.debug("Login: received response [#{response.code}]")
55
51
 
@@ -3,6 +3,7 @@
3
3
  require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
+ require_relative "../../internal/http"
6
7
 
7
8
  module Braintrust
8
9
  class API
@@ -22,7 +23,8 @@ module Braintrust
22
23
  # @param tags [Array<String>, nil] Optional tags
23
24
  # @param metadata [Hash, nil] Optional metadata
24
25
  # @return [Hash] Experiment data with "id", "name", "project_id", etc.
25
- def create(name:, project_id:, ensure_new: true, tags: nil, metadata: nil)
26
+ def create(name:, project_id:, ensure_new: true, tags: nil, metadata: nil,
27
+ dataset_id: nil, dataset_version: nil)
26
28
  uri = URI("#{@state.api_url}/v1/experiment")
27
29
 
28
30
  payload = {
@@ -32,15 +34,15 @@ module Braintrust
32
34
  }
33
35
  payload[:tags] = tags if tags
34
36
  payload[:metadata] = metadata if metadata
37
+ payload[:dataset_id] = dataset_id if dataset_id
38
+ payload[:dataset_version] = dataset_version if dataset_version
35
39
 
36
40
  request = Net::HTTP::Post.new(uri)
37
41
  request["Content-Type"] = "application/json"
38
42
  request["Authorization"] = "Bearer #{@state.api_key}"
39
43
  request.body = JSON.dump(payload)
40
44
 
41
- http = Net::HTTP.new(uri.host, uri.port)
42
- http.use_ssl = (uri.scheme == "https")
43
- response = http.request(request)
45
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
44
46
 
45
47
  unless response.is_a?(Net::HTTPSuccess)
46
48
  raise Error, "HTTP #{response.code} for POST #{uri}: #{response.body}"
@@ -3,6 +3,7 @@
3
3
  require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
+ require_relative "../../internal/http"
6
7
 
7
8
  module Braintrust
8
9
  class API
@@ -26,9 +27,7 @@ module Braintrust
26
27
  request["Authorization"] = "Bearer #{@state.api_key}"
27
28
  request.body = JSON.dump({name: name})
28
29
 
29
- http = Net::HTTP.new(uri.host, uri.port)
30
- http.use_ssl = (uri.scheme == "https")
31
- response = http.request(request)
30
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
32
31
 
33
32
  unless response.is_a?(Net::HTTPSuccess)
34
33
  raise Error, "HTTP #{response.code} for POST #{uri}: #{response.body}"
@@ -98,9 +98,18 @@ module Braintrust
98
98
  # The remote scorer receives all scorer arguments
99
99
  result = api.functions.invoke(id: function_id, input: scorer_input)
100
100
 
101
- # Parse result as float score
102
- # The remote function should return a number
103
- score = result.is_a?(Numeric) ? result.to_f : result.to_s.to_f
101
+ score = case result
102
+ when Hash
103
+ if result.key?("score")
104
+ result["score"].to_f
105
+ else
106
+ raise Error, "Hash result must contain 'score' key"
107
+ end
108
+ when String
109
+ result.to_f
110
+ else
111
+ raise Error, "Unsupported result type: #{result.class}"
112
+ end
104
113
 
105
114
  span.set_attribute("braintrust.output_json", JSON.dump(score))
106
115
  score
@@ -220,8 +220,14 @@ module Braintrust
220
220
  api.login
221
221
 
222
222
  # Resolve dataset to cases if dataset parameter provided
223
+ dataset_id = nil
224
+ dataset_version = nil
225
+
223
226
  if dataset
224
- cases = resolve_dataset(dataset, project, api)
227
+ resolved = resolve_dataset(dataset, project, api)
228
+ cases = resolved[:cases]
229
+ dataset_id = resolved[:dataset_id]
230
+ dataset_version = resolved[:dataset_version]
225
231
  end
226
232
 
227
233
  # Register project and experiment via internal API
@@ -234,7 +240,9 @@ module Braintrust
234
240
  project_id: project_result["id"],
235
241
  ensure_new: !update,
236
242
  tags: tags,
237
- metadata: metadata
243
+ metadata: metadata,
244
+ dataset_id: dataset_id,
245
+ dataset_version: dataset_version
238
246
  )
239
247
 
240
248
  experiment_id = experiment_result["id"]
@@ -292,11 +300,11 @@ module Braintrust
292
300
  end
293
301
  end
294
302
 
295
- # Resolve dataset parameter to an array of case records
303
+ # Resolve dataset parameter to cases with metadata for experiment linking
296
304
  # @param dataset [String, Hash, Dataset] Dataset specifier or instance
297
305
  # @param project [String] Project name (used as default if not specified)
298
306
  # @param api [API] Braintrust API client
299
- # @return [Array<Hash>] Array of case records
307
+ # @return [Hash] Hash with :cases, :dataset_id, and :dataset_version
300
308
  def resolve_dataset(dataset, project, api)
301
309
  limit = nil
302
310
 
@@ -315,7 +323,15 @@ module Braintrust
315
323
  raise ArgumentError, "dataset must be String, Hash, or Dataset, got #{dataset.class}"
316
324
  end
317
325
 
318
- dataset_obj.fetch_all(limit: limit)
326
+ cases = dataset_obj.fetch_all(limit: limit)
327
+
328
+ # Use pinned version if available, otherwise compute from max(_xact_id)
329
+ version = dataset_obj.version
330
+ version ||= cases
331
+ .filter_map { |c| c[:origin] && JSON.parse(c[:origin])["_xact_id"] }
332
+ .max
333
+
334
+ {cases: cases, dataset_id: dataset_obj.id, dataset_version: version}
319
335
  end
320
336
  end
321
337
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "zlib"
6
+ require "stringio"
7
+ require_relative "../logger"
8
+
9
+ module Braintrust
10
+ module Internal
11
+ # HTTP utilities for redirect following and response decompression.
12
+ # Drop-in enhancement for raw Net::HTTP request calls throughout the SDK.
13
+ module Http
14
+ DEFAULT_MAX_REDIRECTS = 5
15
+
16
+ # Execute an HTTP request, following redirects as needed.
17
+ #
18
+ # @param uri [URI] The request URI
19
+ # @param request [Net::HTTPRequest] The prepared request object
20
+ # @param max_redirects [Integer] Maximum number of redirects to follow
21
+ # @return [Net::HTTPResponse] The final response
22
+ # @raise [Braintrust::Error] On too many redirects or missing Location header
23
+ def self.with_redirects(uri, request, max_redirects: DEFAULT_MAX_REDIRECTS)
24
+ response = perform_request(uri, request)
25
+
26
+ redirects = 0
27
+ original_request = request
28
+
29
+ while response.is_a?(Net::HTTPRedirection)
30
+ redirects += 1
31
+ if redirects > max_redirects
32
+ raise Error, "Too many redirects (max #{max_redirects})"
33
+ end
34
+
35
+ location = response["location"]
36
+ unless location
37
+ raise Error, "Redirect response #{response.code} without Location header"
38
+ end
39
+
40
+ redirect_uri = URI(location)
41
+ redirect_uri = uri + redirect_uri unless redirect_uri.host
42
+
43
+ Log.debug("[HTTP] Following #{response.code} redirect to #{redirect_uri}")
44
+
45
+ request = build_redirect_request(response, redirect_uri, original_request, uri)
46
+ uri = redirect_uri
47
+ response = perform_request(uri, request)
48
+ end
49
+
50
+ response
51
+ end
52
+
53
+ # Decompress an HTTP response body in place based on Content-Encoding.
54
+ # No-op if the response has no recognized encoding.
55
+ #
56
+ # @param response [Net::HTTPResponse] The response to decompress
57
+ # @return [void]
58
+ def self.decompress_response!(response)
59
+ encoding = response["content-encoding"]&.downcase
60
+ case encoding
61
+ when "gzip", "x-gzip"
62
+ gz = Zlib::GzipReader.new(StringIO.new(response.body))
63
+ response.body.replace(gz.read)
64
+ gz.close
65
+ response.delete("content-encoding")
66
+ end
67
+ end
68
+
69
+ def self.perform_request(uri, request)
70
+ http = Net::HTTP.new(uri.host, uri.port)
71
+ http.use_ssl = (uri.scheme == "https")
72
+ http.request(request)
73
+ end
74
+ private_class_method :perform_request
75
+
76
+ def self.build_redirect_request(response, redirect_uri, original_request, original_uri)
77
+ if response.code == "307" || response.code == "308"
78
+ request = original_request.class.new(redirect_uri)
79
+ request.body = original_request.body
80
+ request["Content-Type"] = original_request["Content-Type"] if original_request["Content-Type"]
81
+ else
82
+ # 301, 302, 303: follow with GET, no body
83
+ request = Net::HTTP::Get.new(redirect_uri)
84
+ end
85
+
86
+ # Strip Authorization when redirecting to a different host (e.g. S3)
87
+ if original_uri.host == redirect_uri.host
88
+ auth = original_request["Authorization"]
89
+ request["Authorization"] = auth if auth
90
+ end
91
+
92
+ request
93
+ end
94
+ private_class_method :build_redirect_request
95
+ end
96
+ end
97
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "net/http"
4
4
  require_relative "../internal/encoding"
5
+ require_relative "../internal/http"
5
6
  require "uri"
6
7
 
7
8
  module Braintrust
@@ -91,7 +92,8 @@ module Braintrust
91
92
  # att = Braintrust::Trace::Attachment.from_url("https://example.com/image.png")
92
93
  def self.from_url(url)
93
94
  uri = URI.parse(url)
94
- response = Net::HTTP.get_response(uri)
95
+ request = Net::HTTP::Get.new(uri)
96
+ response = Braintrust::Internal::Http.with_redirects(uri, request)
95
97
 
96
98
  unless response.is_a?(Net::HTTPSuccess)
97
99
  raise StandardError, "Failed to fetch URL: #{response.code} #{response.message}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Braintrust
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
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.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Braintrust
@@ -242,6 +242,7 @@ files:
242
242
  - lib/braintrust/eval/summary.rb
243
243
  - lib/braintrust/internal/encoding.rb
244
244
  - lib/braintrust/internal/env.rb
245
+ - lib/braintrust/internal/http.rb
245
246
  - lib/braintrust/internal/origin.rb
246
247
  - lib/braintrust/internal/template.rb
247
248
  - lib/braintrust/internal/thread_pool.rb