bitfab 0.38.3 → 0.40.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: a178d05e7c40055c3ac8dec21a40eb60eb36b08a005dfa55d031c3c3b9cdf129
4
+ data.tar.gz: e71a921a9bad32b967c723245c1187257366073f13aab6f8dca2882d6b758fb8
5
5
  SHA512:
6
- metadata.gz: 960e4f390e26e953cb555a1a359fd7e3b505172a74e513fe69f281dc7e1111b03e8ac93451154d632dd225e36cdf33f5c54dc3dc59dbd7379a7af3672f7fb87b
7
- data.tar.gz: ac599caf319a0dc2814147faee28efd5940609bdc2200d696db4bcb3581fc72fafe443bb50f65204ffb46eea5fa8f0ff9806dd78c42a8d3599e1429263d2ab71
6
+ metadata.gz: 559fed18dd194a527ff477e057f465a172d7266ba05c4e6b3305a693091b0fa1de3847078d8f0df4c6822a4b28b2fd6a0f802eaf2db7d75e38233051f05fc887
7
+ data.tar.gz: f4a718c47568294809eaed27905b2d8b67a3058fc954eff2dac6b10a05ae2219463d1742b40bb0a7725d4979a5c86c2beb37e9e10621a588e007a9a7757d6376
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
@@ -690,7 +717,7 @@ module Bitfab
690
717
  # still rides out with dropped: true, so the server scrubs any sibling
691
718
  # spans that already raced out before the flag was set.
692
719
  trace_dropped = TraceState.get(trace_id)&.dig(:dropped) == true
693
- return nil if trace_dropped
720
+ return if trace_dropped
694
721
 
695
722
  # Human-readable JSON (input/output fields). The *_with_report variants
696
723
  # also report what could not be faithfully captured so a lossy span is
@@ -772,8 +799,8 @@ module Bitfab
772
799
  # mocked) so unmarked spans don't silently shift subsequent marked
773
800
  # spans' indices. Different (key, name) pairs have independent counters.
774
801
  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)
802
+ return if is_root_span
803
+ return unless replay_ctx&.dig(:mock_tree)
777
804
 
778
805
  counters = replay_ctx[:call_counters]
779
806
  counter_key = "#{trace_function_key}:#{span_name}"
@@ -819,6 +846,7 @@ module Bitfab
819
846
  # output, wrap it in a callable value.
820
847
  unless value.respond_to?(:call)
821
848
  return [value, "override"] unless value.equal?(NO_MOCK_OVERRIDE)
849
+
822
850
  next
823
851
  end
824
852
 
@@ -826,6 +854,7 @@ module Bitfab
826
854
  unless mock_entry
827
855
  raise "No recorded span to source output for '#{trace_function_key}'."
828
856
  end
857
+
829
858
  resolve_recorded_output(mock_entry, replay_ctx)
830
859
  end
831
860
  resolved = value.call({node:, inputs: args, kwargs:, get_original_output:})
@@ -847,7 +876,7 @@ module Bitfab
847
876
  unless mock_entry
848
877
  raise StandardError,
849
878
  "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."
879
+ "but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
851
880
  end
852
881
 
853
882
  [resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
@@ -955,9 +984,13 @@ module Bitfab
955
984
  # mock strategy. A missing selected occurrence fails closed.
956
985
  def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
957
986
  Bitfab::Traceable.wrap(
958
- klass, method_name,
987
+ klass,
988
+ method_name,
959
989
  trace_function_key: @trace_function_key,
960
- name:, type:, capture_when:, mock_on_replay:,
990
+ name:,
991
+ type:,
992
+ capture_when:,
993
+ mock_on_replay:,
961
994
  client: @client
962
995
  )
963
996
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.38.3"
4
+ VERSION = "0.40.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"
@@ -119,16 +120,16 @@ module Bitfab
119
120
  # whose source trace carried no DB snapshot reference
120
121
  def current_replay_branch
121
122
  ctx = ReplayContext.current
122
- return nil unless ctx
123
+ return unless ctx
123
124
 
124
125
  lease = ctx[:db_branch_lease]
125
- return nil unless lease
126
+ return unless lease
126
127
 
127
128
  # Surface the Bitfab trace ID (what the customer sees in the dashboard),
128
129
  # not the external one. Falling back to the external ID keeps replays from
129
130
  # external sources working until that path is fully wired.
130
131
  trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
131
- return nil unless trace_id
132
+ return unless trace_id
132
133
 
133
134
  ReplayBranch.new(lease, trace_id, ctx)
134
135
  end
@@ -159,7 +160,7 @@ module Bitfab
159
160
  # succeeded, errored, and item (the object replay already passes to
160
161
  # on_item_finish)
161
162
  def report_replay_progress(progress)
162
- warn "#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}"
163
+ warn("#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}")
163
164
  rescue
164
165
  nil
165
166
  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.40.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