bitfab 0.38.3 → 0.41.0

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: 28a82b98d8327aead543061c18b95a22b2100f89e0289ffdd6ad85a262e08447
4
- data.tar.gz: 18c101e18d509cc2a5fb880210d3d5cfaee0f9311c7521abeaa2716bdeca563f
3
+ metadata.gz: c5e2025cf56735ed8b8ef283be12a9aba5c4a859bb8bfe6d88479292e2fbba61
4
+ data.tar.gz: 37ec3530d9e57447ebf8ec84c257e8a730e3af776efa7de45941806e9c129b8c
5
5
  SHA512:
6
- metadata.gz: 960e4f390e26e953cb555a1a359fd7e3b505172a74e513fe69f281dc7e1111b03e8ac93451154d632dd225e36cdf33f5c54dc3dc59dbd7379a7af3672f7fb87b
7
- data.tar.gz: ac599caf319a0dc2814147faee28efd5940609bdc2200d696db4bcb3581fc72fafe443bb50f65204ffb46eea5fa8f0ff9806dd78c42a8d3599e1429263d2ab71
6
+ metadata.gz: b75605cc3372d3cfddab8a6b18a764137105188902ea05b63a006633c0bf6f1743ca64aac56e19d553b1c75556296928eaf692a9d28cb68e7c0650f2249fbb65
7
+ data.tar.gz: 12b2be619ba694e5f552267a0b8b439cc9cec7f804f6faebe6739e0ebba6e0a1afa0cc413a0a101129158e56b60377c586eba313e4c24e1ddc34692df009fbc4
data/lib/bitfab/client.rb CHANGED
@@ -13,7 +13,7 @@ require_relative "warn_once"
13
13
 
14
14
  module Bitfab
15
15
  class Client
16
- SPAN_TYPES = %w[llm agent function guardrail handoff custom].freeze
16
+ SPAN_TYPES = ["llm", "agent", "function", "guardrail", "handoff", "custom"].freeze
17
17
  UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
18
18
 
19
19
  # Sentinel returned by check_mock_replay when this span should run real
@@ -30,6 +30,9 @@ module Bitfab
30
30
 
31
31
  attr_reader :service_url
32
32
 
33
+ # Dataset operations for the authenticated organization.
34
+ attr_reader :datasets
35
+
33
36
  def initialize(api_key: nil, service_url: nil, enabled: true, strict: false)
34
37
  @api_key_config = api_key
35
38
  @service_url = service_url || DEFAULT_SERVICE_URL
@@ -43,6 +46,7 @@ module Bitfab
43
46
  # The key is NOT read here. HttpClient gets a proc so the key is resolved
44
47
  # at send time, after any env loading has run.
45
48
  @http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
49
+ @datasets = Datasets.new(@http_client)
46
50
  # Mock overrides registered via register_mock_override, applied to every
47
51
  # replay on this client (after any per-call mock_override). Instance
48
52
  # state, no global; clear_mock_overrides resets it.
@@ -157,10 +161,25 @@ module Bitfab
157
161
  name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
158
162
  adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
159
163
  Replay.run(
160
- self, receiver, method_name,
161
- trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
162
- code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
163
- mock_override:, db_branch:, on_item_start:, on_item_finish:,
164
+ self,
165
+ receiver,
166
+ method_name,
167
+ trace_function_key:,
168
+ limit:,
169
+ trace_ids:,
170
+ name:,
171
+ max_concurrency:,
172
+ code_change_description:,
173
+ code_change_files:,
174
+ experiment_group_id:,
175
+ dataset_id:,
176
+ grader_ids:,
177
+ mock:,
178
+ adapt_inputs:,
179
+ mock_override:,
180
+ db_branch:,
181
+ on_item_start:,
182
+ on_item_finish:,
164
183
  on_progress:
165
184
  )
166
185
  end
@@ -203,18 +222,18 @@ module Bitfab
203
222
  match: ->(node) { node[:trace_function_key] == trace_function_key && keyed_match.call(node) },
204
223
  value: keyed_override[:value]
205
224
  }
206
- return nil
225
+ return
207
226
  end
208
227
  if keyed_override.respond_to?(:call)
209
228
  @mock_overrides << {
210
229
  match: ->(node) { node[:trace_function_key] == trace_function_key },
211
230
  value: keyed_override
212
231
  }
213
- return nil
232
+ return
214
233
  end
215
234
  raise ArgumentError,
216
235
  "register_mock_override(trace_function_key, override) requires " \
217
- "a { match:, value: } hash or callable resolver."
236
+ "a { match:, value: } hash or callable resolver."
218
237
  elsif positional.length == 1 && positional[0].is_a?(Hash)
219
238
  override = positional[0]
220
239
  match = override[:match]
@@ -223,7 +242,7 @@ module Bitfab
223
242
  value = override.key?(:value) ? override[:value] : VALUE_UNSET
224
243
  elsif positional.length == 1 && positional[0].respond_to?(:call)
225
244
  @mock_overrides << {match: ->(_) { true }, value: positional[0]}
226
- return nil
245
+ return
227
246
  elsif positional.length == 2
228
247
  match, value = positional
229
248
  end
@@ -232,16 +251,16 @@ module Bitfab
232
251
  unless match.respond_to?(:call)
233
252
  raise ArgumentError,
234
253
  "register_mock_override requires a callable match. Pass (match, value) " \
235
- "positionally, as keywords (match:, value:), or as a { match:, value: } hash. " \
236
- "value may be a flat value or a callable."
254
+ "positionally, as keywords (match:, value:), or as a { match:, value: } hash. " \
255
+ "value may be a flat value or a callable."
237
256
  end
238
257
  # A forgotten value must not silently inject nil. An explicit nil is a
239
258
  # legitimate injected value and passes this guard.
240
259
  if value.equal?(VALUE_UNSET)
241
260
  raise ArgumentError,
242
261
  "register_mock_override requires a value (the second argument, or " \
243
- "value:). It may be a flat value or a callable; pass value: nil " \
244
- "explicitly to inject nil."
262
+ "value:). It may be a flat value or a callable; pass value: nil " \
263
+ "explicitly to inject nil."
245
264
  end
246
265
 
247
266
  @mock_overrides << {match:, value:}
@@ -281,12 +300,13 @@ module Bitfab
281
300
  def get_trace_span(trace_id, id: nil, name: nil, occurrence: "last")
282
301
  validate_trace_id(trace_id)
283
302
  raise ArgumentError, "Provide exactly one of id or name" if id.nil? == name.nil?
303
+
284
304
  validate_span_id(id) unless id.nil?
285
305
  if !name.nil? && (!name.is_a?(String) || name.empty?)
286
306
  raise ArgumentError, "name must be a non-empty string"
287
307
  end
288
308
 
289
- valid_occurrence = %w[first last].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0)
309
+ valid_occurrence = ["first", "last"].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0)
290
310
  unless valid_occurrence
291
311
  raise ArgumentError, 'occurrence must be "first", "last", or a non-negative integer'
292
312
  end
@@ -304,11 +324,11 @@ module Bitfab
304
324
  return yield unless tracing_enabled?
305
325
 
306
326
  resolved_capture_when = capture_when.to_s
307
- unless %w[always nested].include?(resolved_capture_when)
327
+ unless ["always", "nested"].include?(resolved_capture_when)
308
328
  Bitfab.warn_once(
309
329
  "invalid-capture-when:#{trace_function_key}",
310
330
  "unknown capture_when value #{capture_when.inspect}; defaulting to \"always\". " \
311
- "Valid values: \"always\", \"nested\"."
331
+ "Valid values: \"always\", \"nested\"."
312
332
  )
313
333
  resolved_capture_when = "always"
314
334
  end
@@ -357,8 +377,14 @@ module Bitfab
357
377
  call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
358
378
  if call_index
359
379
  mock_result = check_mock_replay(
360
- replay_ctx, trace_function_key, span_name, call_index,
361
- span_type:, args:, kwargs:, mock_on_replay:
380
+ replay_ctx,
381
+ trace_function_key,
382
+ span_name,
383
+ call_index,
384
+ span_type:,
385
+ args:,
386
+ kwargs:,
387
+ mock_on_replay:
362
388
  )
363
389
  if mock_result != MOCK_REPLAY_MISS
364
390
  mocked_output, mock_source = mock_result
@@ -394,7 +420,7 @@ module Bitfab
394
420
  Bitfab.warn_once(
395
421
  "span-setup:#{trace_function_key}",
396
422
  "span setup failed for '#{trace_function_key}'; this call runs untraced. " \
397
- "Your method still executes and returns normally."
423
+ "Your method still executes and returns normally."
398
424
  )
399
425
  return yield
400
426
  end
@@ -410,6 +436,7 @@ module Bitfab
410
436
  # only the first call sends the span. Subsequent calls (e.g. from the
411
437
  # enumerator wrapper after iteration completes) are no-ops.
412
438
  next if finalized
439
+
413
440
  finalized = true
414
441
 
415
442
  begin
@@ -551,7 +578,7 @@ module Bitfab
551
578
  end
552
579
  if @explicitly_enabled && !@api_key_warned
553
580
  @api_key_warned = true
554
- warn "Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing."
581
+ warn("Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing.")
555
582
  end
556
583
  nil
557
584
  end
@@ -622,6 +649,9 @@ module Bitfab
622
649
  "ended_at" => ended_at
623
650
  }
624
651
 
652
+ if trace_state&.dig(:name)
653
+ raw_trace["name"] = trace_state[:name]
654
+ end
625
655
  if trace_state&.dig(:metadata)
626
656
  raw_trace["metadata"] = trace_state[:metadata]
627
657
  end
@@ -690,7 +720,7 @@ module Bitfab
690
720
  # still rides out with dropped: true, so the server scrubs any sibling
691
721
  # spans that already raced out before the flag was set.
692
722
  trace_dropped = TraceState.get(trace_id)&.dig(:dropped) == true
693
- return nil if trace_dropped
723
+ return if trace_dropped
694
724
 
695
725
  # Human-readable JSON (input/output fields). The *_with_report variants
696
726
  # also report what could not be faithfully captured so a lossy span is
@@ -772,8 +802,8 @@ module Bitfab
772
802
  # mocked) so unmarked spans don't silently shift subsequent marked
773
803
  # spans' indices. Different (key, name) pairs have independent counters.
774
804
  def advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
775
- return nil if is_root_span
776
- return nil unless replay_ctx&.dig(:mock_tree)
805
+ return if is_root_span
806
+ return unless replay_ctx&.dig(:mock_tree)
777
807
 
778
808
  counters = replay_ctx[:call_counters]
779
809
  counter_key = "#{trace_function_key}:#{span_name}"
@@ -819,6 +849,7 @@ module Bitfab
819
849
  # output, wrap it in a callable value.
820
850
  unless value.respond_to?(:call)
821
851
  return [value, "override"] unless value.equal?(NO_MOCK_OVERRIDE)
852
+
822
853
  next
823
854
  end
824
855
 
@@ -826,6 +857,7 @@ module Bitfab
826
857
  unless mock_entry
827
858
  raise "No recorded span to source output for '#{trace_function_key}'."
828
859
  end
860
+
829
861
  resolve_recorded_output(mock_entry, replay_ctx)
830
862
  end
831
863
  resolved = value.call({node:, inputs: args, kwargs:, get_original_output:})
@@ -847,7 +879,7 @@ module Bitfab
847
879
  unless mock_entry
848
880
  raise StandardError,
849
881
  "Replay selected span '#{trace_function_key}:#{span_name}' for mocking, " \
850
- "but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
882
+ "but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
851
883
  end
852
884
 
853
885
  [resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
@@ -955,9 +987,13 @@ module Bitfab
955
987
  # mock strategy. A missing selected occurrence fails closed.
956
988
  def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
957
989
  Bitfab::Traceable.wrap(
958
- klass, method_name,
990
+ klass,
991
+ method_name,
959
992
  trace_function_key: @trace_function_key,
960
- name:, type:, capture_when:, mock_on_replay:,
993
+ name:,
994
+ type:,
995
+ capture_when:,
996
+ mock_on_replay:,
961
997
  client: @client
962
998
  )
963
999
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ # Dataset operations for the authenticated organization, reached as
5
+ # `client.datasets`. A dataset is a named bucket of traces scoped to one
6
+ # trace function. Experiments replay against it and its graders score its
7
+ # members. Every method returns the parsed JSON response as a Hash with
8
+ # string keys matching the HTTP API ("traceCount", "addedTraceIds", ...).
9
+ class Datasets
10
+ DEFAULT_RERUN_TIMEOUT_SECONDS = 90
11
+ DEFAULT_RERUN_POLL_INTERVAL_SECONDS = 1
12
+ TERMINAL_RERUN_STATUSES = ["completed", "errored"].freeze
13
+
14
+ def initialize(http_client)
15
+ @http_client = http_client
16
+ end
17
+
18
+ # Create a dataset, or update the one already named this way under the
19
+ # same trace function. The result's "created" reports which happened. A
20
+ # nil description leaves an existing description untouched.
21
+ def save(trace_function_key:, name:, description: nil)
22
+ payload = {"traceFunctionKey" => trace_function_key, "name" => name}
23
+ payload["description"] = description unless description.nil?
24
+ @http_client.request("/api/sdk/datasets", payload)
25
+ end
26
+
27
+ # List datasets, scoped to one trace function when given and
28
+ # organization-wide otherwise.
29
+ def list(trace_function_key: nil)
30
+ query = trace_function_key.nil? ? "" : "?#{URI.encode_www_form("traceFunctionKey" => trace_function_key)}"
31
+ @http_client.get("/api/sdk/datasets#{query}")["datasets"]
32
+ end
33
+
34
+ # Fetch one dataset by id. A dataset outside this organization raises
35
+ # Net::HTTPError with a 404.
36
+ def get(dataset_id)
37
+ @http_client.get(dataset_path(dataset_id))["dataset"]
38
+ end
39
+
40
+ # The ids of every trace in the dataset, the same membership a replay with
41
+ # dataset_id: selects.
42
+ def list_traces(dataset_id)
43
+ @http_client.get(dataset_path(dataset_id, "/traces"))
44
+ end
45
+
46
+ # Add traces to the dataset (1 to 100 ids per call). Traces outside the
47
+ # organization or under another trace function are reported in
48
+ # "skippedTraceIds" rather than failing the call.
49
+ def add_traces(dataset_id, trace_ids)
50
+ @http_client.request(dataset_path(dataset_id, "/traces"), {"traceIds" => trace_ids})
51
+ end
52
+
53
+ # Remove traces from the dataset. The traces themselves are never deleted.
54
+ def remove_traces(dataset_id, trace_ids)
55
+ @http_client.request(dataset_path(dataset_id, "/removeTraces"), {"traceIds" => trace_ids})
56
+ end
57
+
58
+ # Assign graders to the dataset (1 to 100 ids per call). Graders outside
59
+ # the organization or under another trace function are reported in
60
+ # "skippedGraderIds" rather than failing the call.
61
+ def add_graders(dataset_id, grader_ids)
62
+ @http_client.request(dataset_path(dataset_id, "/graders"), {"graderIds" => grader_ids})
63
+ end
64
+
65
+ # Unassign graders from the dataset.
66
+ def remove_graders(dataset_id, grader_ids)
67
+ @http_client.request(dataset_path(dataset_id, "/removeGraders"), {"graderIds" => grader_ids})
68
+ end
69
+
70
+ # Re-run graders over every trace in the dataset. Defaults to every
71
+ # assigned grader, and an unassigned id is rejected. Waits for the run to
72
+ # finish (up to `timeout` seconds) unless `wait:` is false, and returns the
73
+ # last run state seen either way. A request matching an in-flight run
74
+ # joins it.
75
+ def rerun_graders(dataset_id, grader_ids: nil, wait: true,
76
+ timeout: DEFAULT_RERUN_TIMEOUT_SECONDS, poll_interval: DEFAULT_RERUN_POLL_INTERVAL_SECONDS)
77
+ payload = grader_ids.nil? ? {} : {"graderIds" => grader_ids}
78
+ started = @http_client.request(dataset_path(dataset_id, "/rerunGraders"), payload)
79
+ return started unless wait
80
+
81
+ deadline = monotonic_now + timeout
82
+ run = started["run"]
83
+ while !TERMINAL_RERUN_STATUSES.include?(run["status"]) && monotonic_now < deadline
84
+ sleep(poll_interval)
85
+ run = get_grader_rerun(dataset_id, run_id: run["id"]) || run
86
+ end
87
+ {"run" => run, "joinedExisting" => started["joinedExisting"]}
88
+ end
89
+
90
+ # The dataset's active grader re-run, or the run named by run_id:. Returns
91
+ # nil when nothing is active or the run is not this dataset's.
92
+ def get_grader_rerun(dataset_id, run_id: nil)
93
+ query = run_id.nil? ? "" : "?#{URI.encode_www_form("runId" => run_id)}"
94
+ @http_client.get(dataset_path(dataset_id, "/rerunGraders#{query}"))["run"]
95
+ end
96
+
97
+ private
98
+
99
+ def dataset_path(dataset_id, suffix = "")
100
+ "/api/sdk/datasets/#{URI.encode_www_form_component(dataset_id)}#{suffix}"
101
+ end
102
+
103
+ def monotonic_now
104
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
105
+ end
106
+ end
107
+ end
@@ -65,6 +65,14 @@ module Bitfab
65
65
  # Silently ignore - never crash the host app
66
66
  end
67
67
 
68
+ def set_name(name)
69
+ return unless name.is_a?(String) && !name.empty?
70
+
71
+ get_or_create_trace_state[:name] = name
72
+ rescue
73
+ nil
74
+ end
75
+
68
76
  # Set metadata for this trace.
69
77
  # Metadata is stored in the raw trace data. Subsequent calls merge with
70
78
  # existing metadata, with later values taking precedence.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.38.3"
4
+ VERSION = "0.41.0"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "bitfab/serialize"
10
10
  require_relative "bitfab/db_snapshot"
11
11
  require_relative "bitfab/span_context"
12
12
  require_relative "bitfab/http_client"
13
+ require_relative "bitfab/datasets"
13
14
  require_relative "bitfab/mock_override"
14
15
  require_relative "bitfab/replay"
15
16
  require_relative "bitfab/replay_branch"
@@ -46,6 +47,9 @@ module Bitfab
46
47
  # No-op
47
48
  end
48
49
 
50
+ def set_name(_name)
51
+ end
52
+
49
53
  def set_metadata(_metadata)
50
54
  # No-op
51
55
  end
@@ -119,16 +123,16 @@ module Bitfab
119
123
  # whose source trace carried no DB snapshot reference
120
124
  def current_replay_branch
121
125
  ctx = ReplayContext.current
122
- return nil unless ctx
126
+ return unless ctx
123
127
 
124
128
  lease = ctx[:db_branch_lease]
125
- return nil unless lease
129
+ return unless lease
126
130
 
127
131
  # Surface the Bitfab trace ID (what the customer sees in the dashboard),
128
132
  # not the external one. Falling back to the external ID keeps replays from
129
133
  # external sources working until that path is fully wired.
130
134
  trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
131
- return nil unless trace_id
135
+ return unless trace_id
132
136
 
133
137
  ReplayBranch.new(lease, trace_id, ctx)
134
138
  end
@@ -159,7 +163,7 @@ module Bitfab
159
163
  # succeeded, errored, and item (the object replay already passes to
160
164
  # on_item_finish)
161
165
  def report_replay_progress(progress)
162
- warn "#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}"
166
+ warn("#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}")
163
167
  rescue
164
168
  nil
165
169
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.3
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -142,6 +142,7 @@ files:
142
142
  - lib/bitfab/client.rb
143
143
  - lib/bitfab/compress.rb
144
144
  - lib/bitfab/constants.rb
145
+ - lib/bitfab/datasets.rb
145
146
  - lib/bitfab/db_snapshot.rb
146
147
  - lib/bitfab/http_client.rb
147
148
  - lib/bitfab/mock_override.rb