bitfab 0.51.9 → 0.57.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 +4 -4
- data/README.md +4 -4
- data/exe/bitfab-seed +13 -0
- data/lib/bitfab/assertion_categories.rb +42 -0
- data/lib/bitfab/assertions.rb +35 -0
- data/lib/bitfab/baml.rb +208 -0
- data/lib/bitfab/client.rb +157 -50
- data/lib/bitfab/db_snapshot.rb +18 -5
- data/lib/bitfab/detached_trace.rb +46 -0
- data/lib/bitfab/graders.rb +23 -0
- data/lib/bitfab/http_client.rb +124 -35
- data/lib/bitfab/labels.rb +80 -0
- data/lib/bitfab/otel.rb +1 -1
- data/lib/bitfab/replay.rb +92 -23
- data/lib/bitfab/replay_cli.rb +7 -1
- data/lib/bitfab/replay_concurrency.rb +44 -0
- data/lib/bitfab/replay_memory.rb +111 -0
- data/lib/bitfab/replay_processes.rb +218 -0
- data/lib/bitfab/replay_registry.rb +100 -15
- data/lib/bitfab/seed.rb +128 -0
- data/lib/bitfab/seed_cli.rb +67 -0
- data/lib/bitfab/seed_context.rb +21 -0
- data/lib/bitfab/simulation_plan.rb +307 -0
- data/lib/bitfab/span_context.rb +9 -4
- data/lib/bitfab/span_origin.rb +13 -0
- data/lib/bitfab/subtree.rb +214 -26
- data/lib/bitfab/thread_propagation.rb +111 -0
- data/lib/bitfab/traceable.rb +85 -155
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +9 -2
- metadata +32 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cd6fe2aaa2d03dea82e1d6a14b63e5a60eec3169c33489ce54795840ab6db8c
|
|
4
|
+
data.tar.gz: 1637726e4aa1cfa7940ecb65e1b10684b0d1236885a3c1ddd0d3748c8c7a874f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 391ca0dbc155bf7826f8ff4245d305d6d426cd8cd330a2c92564de8eac72ee229d928faad6d74f72f3d7163cf8a950fab1c6dfc96c5ddefdbd215ba0167382a1
|
|
7
|
+
data.tar.gz: c8a4c5172ecb6f78b309303062167073384bbdd7079c5fc5618200b877526a44bd495f85b2e6120b9a602542f883b80d2872ba2519f81109d3e17d83977b301f
|
data/README.md
CHANGED
|
@@ -235,13 +235,13 @@ class OrderPipeline
|
|
|
235
235
|
end
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting
|
|
238
|
+
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting a span limit stops new capture but allows already-open spans in every Fiber to finish. `max_depth:` (default `30`), `max_captured_subtree_spans:` (default `512`), `max_spans:` (default `2048`), `exclude:`, and `include_wrappers:` control the subtree. `max_spans:` is a strict cap on the total number of spans one trace root sends. It counts the root span itself, discovered calls, `bitfab_node` calls, nested trace roots, and spans whose capture is on or off, including spans sent without inputs and outputs because the sim plan could not be read. Nothing past it is sent. It must be at least 1, and a lower value raises `ArgumentError`. `max_captured_subtree_spans:` caps discovered calls sent with their inputs and outputs. It does not count `bitfab_node` calls, nested trace roots, or spans the loaded sim plan turned capture off for, so those keep recording past it up to `max_spans:`. Spans sent without inputs and outputs only because the sim plan could not be read still count toward it. Past it, further discovered calls with capture on record nothing. It is never higher than `max_spans:`. Once the plan has loaded, the SDK never builds the inputs or outputs of a span whose capture it turned off. The client starts reading the sim plan when it is built. A `bitfab_trace` root that starts while that first read is still running waits for it, for at most five seconds. Once the first read has finished, whether it succeeded or failed, no later root waits. Nested roots and the calls beneath a root never wait, and nothing waits when the plan is disabled. If that first read fails, spans other than each trace's root span are sent without inputs and outputs until a read succeeds, and they still count toward both limits. Automatically captured calls are not replay mock targets. Declare a call with `bitfab_node` when replay must substitute it. The configured node records once in each enclosing trace. Mixing explicit spans with subtree tracing raises `Bitfab::MixedTracingError`.
|
|
239
239
|
|
|
240
240
|
When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and `Subtree capture ended before the call returned` as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use.
|
|
241
241
|
|
|
242
242
|
Returning an `Enumerator` keeps capture pending until iteration finishes or raises, including calls already paused in child Fibers. Capture is inactive between the initial return and consumption. Automatic spans record lazy return values as placeholders without iterating them; the traced root records the yielded values.
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
Each nested `bitfab_trace` starts an independent trace during ordinary capture, including recursion and calls using the same key. Every enclosing trace retains its own recording of the shared subtree. The enclosing span and nested root carry reciprocal trace and span links. Each trace applies its own limits and exclusions. During replay and seed execution, nested annotations remain in the managed trace.
|
|
245
245
|
|
|
246
246
|
Configure a discovered method with `bitfab_node`. A node never creates a span or trace by itself; the active `bitfab_trace` owns its key, limits, parent, and lifecycle:
|
|
247
247
|
|
|
@@ -258,7 +258,7 @@ end
|
|
|
258
258
|
|
|
259
259
|
Nodes support `name:`, `type:`, `capture:`, `test_run_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
|
|
260
260
|
|
|
261
|
-
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After
|
|
261
|
+
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After a span limit is reached, it stops reading inputs and outputs immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
|
|
262
262
|
|
|
263
263
|
### Input/Output Capture
|
|
264
264
|
|
|
@@ -463,7 +463,7 @@ Bitfab.reset!
|
|
|
463
463
|
## Thread Safety
|
|
464
464
|
|
|
465
465
|
- Each thread has its own span context stack (using `Thread.current`)
|
|
466
|
-
-
|
|
466
|
+
- Set `trace_across_threads: true` to propagate explicit spans and replay mocks to submitted threads and optional `concurrent-ruby` thread-pool jobs
|
|
467
467
|
- Background span sending is thread-safe
|
|
468
468
|
|
|
469
469
|
## Examples
|
data/exe/bitfab-seed
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bitfab"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
path, args = Bitfab::ReplayCommand.parse(ARGV)
|
|
8
|
+
registry = Bitfab::ReplayCommand.load_registry(path)
|
|
9
|
+
Bitfab::SeedCli.run(registry, argv: args)
|
|
10
|
+
rescue => error
|
|
11
|
+
warn error.message
|
|
12
|
+
exit 1
|
|
13
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class AssertionCategories
|
|
7
|
+
PATH = "/api/sdk/assertionCategories"
|
|
8
|
+
JUSTIFICATION_UNSET = Object.new.freeze
|
|
9
|
+
private_constant :PATH
|
|
10
|
+
private_constant :JUSTIFICATION_UNSET
|
|
11
|
+
|
|
12
|
+
def initialize(http_client)
|
|
13
|
+
@http_client = http_client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def save(title, id: nil, description: nil, justification: JUSTIFICATION_UNSET)
|
|
17
|
+
payload = {"title" => title}
|
|
18
|
+
payload["id"] = id unless id.nil?
|
|
19
|
+
payload["description"] = description unless description.nil?
|
|
20
|
+
payload["justification"] = justification unless justification.equal?(JUSTIFICATION_UNSET)
|
|
21
|
+
@http_client.request(PATH, payload)["category"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(id)
|
|
25
|
+
@http_client.get("#{PATH}/#{encode(id)}")["category"]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def list
|
|
29
|
+
@http_client.get(PATH)["categories"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(id)
|
|
33
|
+
@http_client.request("#{PATH}/#{encode(id)}", {}, method: "DELETE")["category"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def encode(value)
|
|
39
|
+
URI.encode_www_form_component(value).gsub("+", "%20")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class Traces
|
|
7
|
+
def initialize(http_client)
|
|
8
|
+
@http_client = http_client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get_assertions(trace_id)
|
|
12
|
+
@http_client.get(assertions_path(trace_id))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def save_assertions(trace_id, assertions, source: "agent")
|
|
16
|
+
save_assertions_all([{"traceId" => trace_id, "assertions" => assertions}], source:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def save_assertions_all(updates, source: "agent")
|
|
20
|
+
return [] if updates.empty?
|
|
21
|
+
|
|
22
|
+
@http_client.request("/api/sdk/traces/assertions", {"updates" => updates, "source" => source})["assertions"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def archive_assertions(trace_id, assertion_ids)
|
|
26
|
+
@http_client.request(assertions_path(trace_id, "/archive"), {"assertionIds" => assertion_ids})["archived"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def assertions_path(trace_id, suffix = "")
|
|
32
|
+
"/api/sdk/traces/#{URI.encode_www_form_component(trace_id).gsub("+", "%20")}/assertions#{suffix}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/bitfab/baml.rb
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
module BAML
|
|
7
|
+
ALLOWED_ENV_KEYS = %w[OPENAI_API_KEY].freeze
|
|
8
|
+
COLLECTOR_FIELDS = %i[logs last usage function_name log_type timing calls selected_call tags
|
|
9
|
+
selected http_request http_response provider client_name input_tokens output_tokens
|
|
10
|
+
cached_input_tokens start_time_utc_ms duration_ms url method headers body status].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def load_runtime
|
|
15
|
+
require "baml" unless defined?(::Baml::DynamicStruct)
|
|
16
|
+
::Baml
|
|
17
|
+
rescue LoadError
|
|
18
|
+
raise LoadError, "BAML is required for local execution. Install the baml gem matching your generated client."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parameters(source)
|
|
22
|
+
signature = source.match(/function\s+\w+\s*\(([^)]*)\)\s*->/)&.[](1)
|
|
23
|
+
return {} unless signature
|
|
24
|
+
|
|
25
|
+
depth = 0
|
|
26
|
+
parts = [+""]
|
|
27
|
+
signature.each_char do |char|
|
|
28
|
+
depth += 1 if char == "<"
|
|
29
|
+
depth -= 1 if char == ">"
|
|
30
|
+
if char == "," && depth.zero?
|
|
31
|
+
parts << +""
|
|
32
|
+
else
|
|
33
|
+
parts.last << char
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
parts.filter_map do |part|
|
|
37
|
+
match = part.strip.match(/\A(\w+)\s*:\s*(.+)\z/)
|
|
38
|
+
[match[1], match[2].strip.delete_suffix("?")] if match
|
|
39
|
+
end.to_h
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def coerce_inputs(inputs, types)
|
|
43
|
+
inputs.to_h do |key, value|
|
|
44
|
+
type = types[key.to_s]
|
|
45
|
+
[key, (value.is_a?(String) && type) ? coerce(value, type) : value]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def coerce(value, type)
|
|
50
|
+
case type
|
|
51
|
+
when "string" then value
|
|
52
|
+
when "int" then Integer(value, 10)
|
|
53
|
+
when "float" then Float(value)
|
|
54
|
+
when "bool"
|
|
55
|
+
return true if value.downcase == "true"
|
|
56
|
+
return false if value.downcase == "false"
|
|
57
|
+
value
|
|
58
|
+
else
|
|
59
|
+
parsed = JSON.parse(value)
|
|
60
|
+
(type.end_with?("[]") && !parsed.is_a?(Array)) ? value : parsed
|
|
61
|
+
end
|
|
62
|
+
rescue ArgumentError, JSON::ParserError
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def client_name(provider, model)
|
|
67
|
+
prefix = {"openai" => "OpenAI", "anthropic" => "Anthropic", "google" => "Google"}[provider] || provider.capitalize
|
|
68
|
+
"#{prefix}_#{model.sub(/^gpt-/, "GPT").tr(".-", "__")}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def prepare_source(source, providers)
|
|
72
|
+
return source if source.include?("client<llm> OpenAI_")
|
|
73
|
+
|
|
74
|
+
clients = providers.flat_map do |provider|
|
|
75
|
+
provider.fetch("models").map do |model|
|
|
76
|
+
name = model.fetch("model")
|
|
77
|
+
temperature = (provider["provider"] == "openai" && name.start_with?("gpt-4.1", "gpt-4o")) ? "\n temperature 0" : ""
|
|
78
|
+
"client<llm> #{client_name(provider.fetch("provider"), name)} {\n provider #{provider.fetch("provider")}\n options {\n model #{name.to_json}\n api_key env.#{provider.fetch("apiKeyEnv")}#{temperature}\n }\n}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
"#{clients.join("\n\n")}\n\n#{source}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def run(source, inputs, providers, env_vars)
|
|
85
|
+
name = source.match(/function\s+(\w+)\s*\(/)&.[](1)
|
|
86
|
+
raise ArgumentError, "No function found in BAML source" unless name
|
|
87
|
+
|
|
88
|
+
baml = load_runtime
|
|
89
|
+
env = env_vars.select { |key, value| ALLOWED_ENV_KEYS.include?(key) && value.is_a?(String) }
|
|
90
|
+
runtime = baml::Ffi::BamlRuntime.from_files("/tmp/bitfab_baml_runtime", {"source.baml" => prepare_source(source, providers)}, env)
|
|
91
|
+
collector = baml::Collector.new(name: "bitfab-collector")
|
|
92
|
+
response = runtime.call_function(name, coerce_inputs(inputs, parameters(source)), runtime.create_context_manager, nil, nil, [collector], env)
|
|
93
|
+
types = Module.new
|
|
94
|
+
result = response.parsed_using_types(types, types, false)
|
|
95
|
+
[result, serialize_collector(collector)]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def serialize_collector(value, depth = 0)
|
|
99
|
+
return nil if depth > 8
|
|
100
|
+
return value if value.nil? || value == true || value == false || value.is_a?(Numeric) || value.is_a?(String)
|
|
101
|
+
return value.map { |item| serialize_collector(item, depth + 1) } if value.is_a?(Array)
|
|
102
|
+
return value.transform_values { |item| serialize_collector(item, depth + 1) } if value.is_a?(Hash)
|
|
103
|
+
return serialize_collector(value.json, depth + 1) if value.respond_to?(:json)
|
|
104
|
+
|
|
105
|
+
COLLECTOR_FIELDS.each_with_object({}) do |field, result|
|
|
106
|
+
result[field.to_s] = serialize_collector(value.public_send(field), depth + 1) if value.respond_to?(field)
|
|
107
|
+
rescue
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
rescue
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def enrich_span(collector)
|
|
115
|
+
log = collector.last
|
|
116
|
+
calls = log&.calls || []
|
|
117
|
+
call = calls.find(&:selected) || calls.first
|
|
118
|
+
body = call&.http_request&.body&.json
|
|
119
|
+
body = {} unless body.is_a?(Hash)
|
|
120
|
+
messages = body["messages"]
|
|
121
|
+
if messages.is_a?(Array)
|
|
122
|
+
rendered = messages.filter_map do |message|
|
|
123
|
+
next unless message.is_a?(Hash) && message["role"].is_a?(String)
|
|
124
|
+
{role: message["role"], content: message["content"].is_a?(String) ? message["content"] : message["content"].to_json}
|
|
125
|
+
end
|
|
126
|
+
Bitfab.current_span.set_prompt(rendered.to_json) unless rendered.empty?
|
|
127
|
+
end
|
|
128
|
+
usage = collector.usage
|
|
129
|
+
metadata = {
|
|
130
|
+
provider: call&.provider,
|
|
131
|
+
model: body["model"] || call&.http_request&.url&.match(%r{/models/([^/:]+)})&.[](1),
|
|
132
|
+
inputTokens: usage&.input_tokens || call&.usage&.input_tokens,
|
|
133
|
+
outputTokens: usage&.output_tokens || call&.usage&.output_tokens,
|
|
134
|
+
durationMs: log&.timing&.duration_ms
|
|
135
|
+
}.compact
|
|
136
|
+
Bitfab.current_span.add_context(metadata) unless metadata.empty?
|
|
137
|
+
rescue
|
|
138
|
+
nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class Wrapper
|
|
142
|
+
attr_reader :collector
|
|
143
|
+
|
|
144
|
+
def initialize(client, method_name, on_collector)
|
|
145
|
+
@client = client
|
|
146
|
+
@method_name = method_name
|
|
147
|
+
@on_collector = on_collector
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def call(*args, **kwargs, &block)
|
|
151
|
+
@collector = nil
|
|
152
|
+
begin
|
|
153
|
+
baml = BAML.load_runtime
|
|
154
|
+
rescue LoadError
|
|
155
|
+
return @client.public_send(@method_name, *args, **kwargs, &block)
|
|
156
|
+
end
|
|
157
|
+
collector = baml::Collector.new(name: "bitfab-baml-tracing")
|
|
158
|
+
tracked = @client.with_options(collector:)
|
|
159
|
+
result = tracked.public_send(@method_name, *args, **kwargs, &block)
|
|
160
|
+
@collector = collector
|
|
161
|
+
BAML.enrich_span(collector)
|
|
162
|
+
begin
|
|
163
|
+
@on_collector&.call(collector)
|
|
164
|
+
rescue
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
result
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class Client
|
|
173
|
+
# Execute a stored BAML prompt locally and return its parsed output.
|
|
174
|
+
def call(method_name, **inputs)
|
|
175
|
+
version = @http_client.request("/api/sdk/functions/lookup", {"name" => method_name})
|
|
176
|
+
raise ArgumentError, "Function #{method_name.inspect} not found. Create it at #{@service_url}/functions" unless version["id"]
|
|
177
|
+
raise ArgumentError, "Function #{method_name.inspect} has no prompt configured." if version["prompt"].to_s.empty?
|
|
178
|
+
|
|
179
|
+
result, collector = BAML.run(version["prompt"], inputs, version["providers"] || [], @env_vars)
|
|
180
|
+
output = begin
|
|
181
|
+
result.is_a?(String) ? result : result.to_json
|
|
182
|
+
rescue
|
|
183
|
+
result.to_s
|
|
184
|
+
end
|
|
185
|
+
@http_client.send_internal_trace(version["id"], {"result" => output, "inputs" => inputs, "rawCollector" => collector, "source" => "ruby-sdk"})
|
|
186
|
+
result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Add rendered prompt and token metadata to the active span around a BAML method.
|
|
190
|
+
def wrap_baml(method_or_client, method = nil, on_collector: nil)
|
|
191
|
+
target = method ? method_or_client : @baml_client
|
|
192
|
+
actual = method || method_or_client
|
|
193
|
+
target ||= actual.receiver if actual.is_a?(Method)
|
|
194
|
+
raise ArgumentError, "baml_client is required for wrap_baml." unless target
|
|
195
|
+
name = actual.is_a?(Method) ? actual.name : actual
|
|
196
|
+
raise ArgumentError, "wrap_baml requires a named BAML method." unless name.is_a?(String) || name.is_a?(Symbol)
|
|
197
|
+
|
|
198
|
+
BAML::Wrapper.new(target, name, on_collector)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class BitfabFunction
|
|
203
|
+
# BAML collector wrappers enrich the currently active span under any key.
|
|
204
|
+
def wrap_baml(*args, **kwargs)
|
|
205
|
+
@client.wrap_baml(*args, **kwargs)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|