braintrust 0.4.1 → 0.5.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/lib/braintrust/contrib/rails/server/templates/initializer.rb.tt +19 -0
- data/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb +51 -11
- data/lib/braintrust/dataset.rb +3 -3
- data/lib/braintrust/eval/runner.rb +34 -7
- data/lib/braintrust/eval.rb +1 -1
- data/lib/braintrust/internal/origin.rb +14 -14
- data/lib/braintrust/scorer.rb +2 -2
- data/lib/braintrust/server/services/eval_service.rb +11 -3
- data/lib/braintrust/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38aa89e0aba8b3df517d42a95d016cb09cbfbaeac91d16d4caed00f0779415eb
|
|
4
|
+
data.tar.gz: fd800de8e09a8ac7ca93342d65abd6c19dcdc5bffe2421dd5cf921e1b6b06645
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82c01479c75cbb51bc79a67af1b91c46af0fdefc2895c05a54837fb38a2bd501a5e462e1cff953fc83e189ad9e1da891e39814077bb0cd03fd66970eccb285a9
|
|
7
|
+
data.tar.gz: '0757861a2df4962b7cdcff628c5638fe361e33b78ba76417dd3c1087c557fffc7920a857a3916f95bc65e3d6f8bb676afb848a27a6fef28dc987b0d7e237c922'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "braintrust/contrib/rails/server"
|
|
2
|
+
|
|
3
|
+
Braintrust::Contrib::Rails::Server::Engine.configure do |config|
|
|
4
|
+
config.evaluators = {
|
|
5
|
+
<% if @evaluators.empty? -%>
|
|
6
|
+
# Add evaluator instances here, for example:
|
|
7
|
+
# "food-classifier" => FoodClassifier.new
|
|
8
|
+
<% else -%>
|
|
9
|
+
<% @evaluators.each_with_index do |evaluator, index| -%>
|
|
10
|
+
"<%= evaluator[:slug] %>" => <%= evaluator[:class_name] %>.new<%= "," unless index == @evaluators.length - 1 %>
|
|
11
|
+
<% end -%>
|
|
12
|
+
<% end -%>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Default is :clerk_token. Use :none only when developing locally without
|
|
16
|
+
# incoming request authentication; outgoing Braintrust API calls still need
|
|
17
|
+
# a valid Braintrust API key.
|
|
18
|
+
config.auth = :clerk_token
|
|
19
|
+
end
|
|
@@ -186,17 +186,17 @@ module Braintrust
|
|
|
186
186
|
::RubyLLM::Providers::OpenAI::Tools.tool_for(tool)
|
|
187
187
|
elsif defined?(::RubyLLM::Providers::Anthropic) && provider.is_a?(::RubyLLM::Providers::Anthropic)
|
|
188
188
|
::RubyLLM::Providers::Anthropic::Tools.tool_for(tool)
|
|
189
|
-
elsif tool
|
|
189
|
+
elsif tool_params_schema(tool)
|
|
190
190
|
build_basic_tool_schema(tool)
|
|
191
191
|
else
|
|
192
192
|
build_minimal_tool_schema(tool)
|
|
193
193
|
end
|
|
194
194
|
rescue NameError, ArgumentError => e
|
|
195
195
|
Braintrust::Log.debug("Failed to extract tool schema using provider-specific method: #{e.class.name}: #{e.message}")
|
|
196
|
-
tool_schema = (tool
|
|
196
|
+
tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
|
|
197
197
|
end
|
|
198
198
|
else
|
|
199
|
-
tool_schema = (tool
|
|
199
|
+
tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool)
|
|
200
200
|
end
|
|
201
201
|
|
|
202
202
|
# Strip RubyLLM-specific fields to match native OpenAI format
|
|
@@ -217,6 +217,19 @@ module Braintrust
|
|
|
217
217
|
tool_schema
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
+
# A tool's JSON Schema, across ruby_llm versions.
|
|
221
|
+
# 1.x exposes it as params_schema; 2.0 renamed it to parameters_schema.
|
|
222
|
+
# @param tool [Object] the RubyLLM tool
|
|
223
|
+
# @return [Hash, nil] the schema, or nil when the tool declares none
|
|
224
|
+
def tool_params_schema(tool)
|
|
225
|
+
%i[params_schema parameters_schema].each do |name|
|
|
226
|
+
next unless tool.respond_to?(name)
|
|
227
|
+
schema = tool.public_send(name)
|
|
228
|
+
return schema if schema
|
|
229
|
+
end
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
220
233
|
# Build a basic tool schema with parameters
|
|
221
234
|
def build_basic_tool_schema(tool)
|
|
222
235
|
{
|
|
@@ -224,7 +237,7 @@ module Braintrust
|
|
|
224
237
|
"function" => {
|
|
225
238
|
"name" => tool.name.to_s,
|
|
226
239
|
"description" => tool.description,
|
|
227
|
-
"parameters" => tool
|
|
240
|
+
"parameters" => tool_params_schema(tool)
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
243
|
end
|
|
@@ -255,10 +268,11 @@ module Braintrust
|
|
|
255
268
|
# Handle content
|
|
256
269
|
if msg.respond_to?(:content) && msg.content
|
|
257
270
|
raw_content = msg.content
|
|
271
|
+
attachments = extract_attachments(msg)
|
|
258
272
|
|
|
259
|
-
#
|
|
260
|
-
formatted["content"] = if
|
|
261
|
-
format_multipart_content(raw_content)
|
|
273
|
+
# Include attachments alongside the text when present (issue #71)
|
|
274
|
+
formatted["content"] = if attachments.any?
|
|
275
|
+
format_multipart_content(content_text(raw_content), attachments)
|
|
262
276
|
else
|
|
263
277
|
format_simple_content(raw_content, msg.role.to_s)
|
|
264
278
|
end
|
|
@@ -278,17 +292,43 @@ module Braintrust
|
|
|
278
292
|
formatted
|
|
279
293
|
end
|
|
280
294
|
|
|
295
|
+
# Collect a message's attachments.
|
|
296
|
+
#
|
|
297
|
+
# ruby_llm 1.x wraps text and attachments in a Content object hanging off
|
|
298
|
+
# the message; 2.0 removed Content, leaving content a plain String and
|
|
299
|
+
# exposing attachments on the message itself. Support both.
|
|
300
|
+
#
|
|
301
|
+
# @param msg [Object] the RubyLLM message
|
|
302
|
+
# @return [Array] the message's attachments, empty when there are none
|
|
303
|
+
def extract_attachments(msg)
|
|
304
|
+
content = msg.content if msg.respond_to?(:content)
|
|
305
|
+
if content.respond_to?(:attachments)
|
|
306
|
+
from_content = Array(content.attachments)
|
|
307
|
+
return from_content if from_content.any?
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
msg.respond_to?(:attachments) ? Array(msg.attachments) : []
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Extract the plain text of a message's content.
|
|
314
|
+
# @param raw_content [Object] String, or a 1.x Content object
|
|
315
|
+
# @return [Object] the text
|
|
316
|
+
def content_text(raw_content)
|
|
317
|
+
raw_content.respond_to?(:text) ? raw_content.text : raw_content
|
|
318
|
+
end
|
|
319
|
+
|
|
281
320
|
# Format multipart content with text and attachments
|
|
282
|
-
# @param
|
|
321
|
+
# @param text [Object] the message text
|
|
322
|
+
# @param attachments [Array] the message's attachments
|
|
283
323
|
# @return [Array<Hash>] array of content parts
|
|
284
|
-
def format_multipart_content(
|
|
324
|
+
def format_multipart_content(text, attachments)
|
|
285
325
|
content_parts = []
|
|
286
326
|
|
|
287
327
|
# Add text part
|
|
288
|
-
content_parts << {"type" => "text", "text" =>
|
|
328
|
+
content_parts << {"type" => "text", "text" => text} if text
|
|
289
329
|
|
|
290
330
|
# Add attachment parts (convert to Braintrust format)
|
|
291
|
-
|
|
331
|
+
attachments.each do |attachment|
|
|
292
332
|
content_parts << format_attachment_for_input(attachment)
|
|
293
333
|
end
|
|
294
334
|
|
data/lib/braintrust/dataset.rb
CHANGED
|
@@ -166,14 +166,14 @@ module Braintrust
|
|
|
166
166
|
record
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
# Build origin
|
|
169
|
+
# Build origin pointer for tracing/linking
|
|
170
170
|
# @param raw [Hash] Raw record from API
|
|
171
171
|
# @param dataset_id [String] Dataset ID (fallback if not in record)
|
|
172
|
-
# @return [
|
|
172
|
+
# @return [Hash, nil] Origin pointer, or nil if record lacks required fields
|
|
173
173
|
def build_origin(raw, dataset_id)
|
|
174
174
|
return nil unless raw["id"] && raw["_xact_id"]
|
|
175
175
|
|
|
176
|
-
Internal::Origin.
|
|
176
|
+
Internal::Origin.build(
|
|
177
177
|
object_type: "dataset",
|
|
178
178
|
object_id: raw["dataset_id"] || dataset_id,
|
|
179
179
|
id: raw["id"],
|
|
@@ -27,6 +27,11 @@ module Braintrust
|
|
|
27
27
|
@eval_context = eval_context
|
|
28
28
|
@tracer = eval_context.tracer_provider.tracer("braintrust-eval")
|
|
29
29
|
|
|
30
|
+
# Whether any scorer/classifier can receive `trace:`. Computed once: the
|
|
31
|
+
# per-case flush that makes traces queryable over BTQL is a real network
|
|
32
|
+
# wait, so it's only worth paying when something will actually consume it.
|
|
33
|
+
@needs_trace = (eval_context.scorers + eval_context.classifiers).any? { |c| wants_trace?(c) }
|
|
34
|
+
|
|
30
35
|
# Mutexes for thread-safe result collection
|
|
31
36
|
@score_mutex = Mutex.new
|
|
32
37
|
@classification_mutex = Mutex.new
|
|
@@ -55,6 +60,11 @@ module Braintrust
|
|
|
55
60
|
# Convert Queue to Array after all threads complete
|
|
56
61
|
error_array = [].tap { |a| a << errors.pop until errors.empty? }
|
|
57
62
|
|
|
63
|
+
# Deliver any spans still buffered. Callers that supply their own
|
|
64
|
+
# tracer_provider get no at_exit hook (see Trace.setup), so without this
|
|
65
|
+
# the tail of the run would wait on the processor's schedule delay.
|
|
66
|
+
flush_spans
|
|
67
|
+
|
|
58
68
|
# Calculate duration
|
|
59
69
|
duration = Time.now - start_time
|
|
60
70
|
|
|
@@ -95,7 +105,7 @@ module Braintrust
|
|
|
95
105
|
set_json_attr(eval_span, "braintrust.expected_json", kase.expected) unless kase.expected.nil?
|
|
96
106
|
set_json_attr(eval_span, "braintrust.metadata", kase.metadata) if kase.metadata
|
|
97
107
|
eval_span.set_attribute("braintrust.tags", kase.tags) if kase.tags
|
|
98
|
-
eval_span
|
|
108
|
+
set_json_attr(eval_span, "braintrust.origin", kase.origin) if kase.origin
|
|
99
109
|
|
|
100
110
|
# Run task
|
|
101
111
|
begin
|
|
@@ -108,9 +118,11 @@ module Braintrust
|
|
|
108
118
|
next
|
|
109
119
|
end
|
|
110
120
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
# Build the trace, then flush spans so they're queryable via BTQL.
|
|
122
|
+
# Both are skipped unless a scorer/classifier declared `trace:` and the
|
|
123
|
+
# trace is actually resolvable (build_trace returns nil in local-only mode).
|
|
124
|
+
kase.trace = build_trace(eval_span) if @needs_trace
|
|
125
|
+
flush_spans if kase.trace
|
|
114
126
|
|
|
115
127
|
# Run scorers
|
|
116
128
|
begin
|
|
@@ -229,6 +241,23 @@ module Braintrust
|
|
|
229
241
|
end
|
|
230
242
|
end
|
|
231
243
|
|
|
244
|
+
# Whether a callable can receive `trace:`: it either declares the keyword
|
|
245
|
+
# or accepts arbitrary kwargs and may forward it. Reuses the #call_parameters
|
|
246
|
+
# introspection Internal::Callable::KeywordFilter already depends on.
|
|
247
|
+
# @param callable [Scorer, Classifier]
|
|
248
|
+
# @return [Boolean]
|
|
249
|
+
def wants_trace?(callable)
|
|
250
|
+
return true unless callable.respond_to?(:call_parameters)
|
|
251
|
+
|
|
252
|
+
callable.call_parameters.any? { |type, name| name == :trace || type == :keyrest }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Force the tracer provider to export buffered spans, if it supports it.
|
|
256
|
+
def flush_spans
|
|
257
|
+
provider = eval_context.tracer_provider
|
|
258
|
+
provider.force_flush if provider.respond_to?(:force_flush)
|
|
259
|
+
end
|
|
260
|
+
|
|
232
261
|
# Build a lazy Trace for a case, backed by BTQL.
|
|
233
262
|
# Returns nil when state or experiment_id are unavailable (local-only mode).
|
|
234
263
|
# @param eval_span [OpenTelemetry::Trace::Span] The eval span for this case
|
|
@@ -275,9 +304,7 @@ module Braintrust
|
|
|
275
304
|
def report_progress(eval_span, kase, **fields)
|
|
276
305
|
return unless eval_context.on_progress
|
|
277
306
|
progress = {"id" => eval_span.context.hex_span_id}.merge(fields.transform_keys(&:to_s))
|
|
278
|
-
if kase.origin
|
|
279
|
-
progress["origin"] = kase.origin.is_a?(String) ? JSON.parse(kase.origin) : kase.origin
|
|
280
|
-
end
|
|
307
|
+
progress["origin"] = kase.origin if kase.origin
|
|
281
308
|
eval_context.on_progress.call(progress)
|
|
282
309
|
rescue => e
|
|
283
310
|
Braintrust.logger.warn("on_progress callback error: #{e.message}")
|
data/lib/braintrust/eval.rb
CHANGED
|
@@ -336,7 +336,7 @@ module Braintrust
|
|
|
336
336
|
# Use pinned version if available, otherwise compute from max(_xact_id)
|
|
337
337
|
version = dataset_obj.version
|
|
338
338
|
version ||= cases
|
|
339
|
-
.filter_map { |c| c[:origin]
|
|
339
|
+
.filter_map { |c| c[:origin]&.dig("_xact_id") }
|
|
340
340
|
.max
|
|
341
341
|
|
|
342
342
|
{cases: cases, dataset_id: dataset_obj.id, dataset_version: version}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module Braintrust
|
|
6
4
|
module Internal
|
|
7
|
-
# Origin
|
|
8
|
-
#
|
|
5
|
+
# Origin builds source object pointers, which link spans back to the record
|
|
6
|
+
# they came from (e.g., a dataset row). Pointers stay Hashes internally and
|
|
7
|
+
# are serialized once at the boundary that needs them, so that inbound
|
|
8
|
+
# pointers from the wire and ones we build here have the same shape.
|
|
9
9
|
module Origin
|
|
10
|
-
#
|
|
10
|
+
# Build an origin pointer
|
|
11
11
|
# @param object_type [String] Type of source object (e.g., "dataset", "playground_logs")
|
|
12
12
|
# @param object_id [String] ID of the source object
|
|
13
13
|
# @param id [String] ID of the specific record within the source
|
|
14
14
|
# @param xact_id [String] Transaction ID
|
|
15
15
|
# @param created [String, nil] Creation timestamp
|
|
16
|
-
# @return [
|
|
17
|
-
def self.
|
|
18
|
-
|
|
19
|
-
object_type
|
|
20
|
-
object_id
|
|
21
|
-
id
|
|
22
|
-
_xact_id
|
|
23
|
-
created
|
|
24
|
-
}
|
|
16
|
+
# @return [Hash] Origin pointer with string keys, matching the wire format
|
|
17
|
+
def self.build(object_type:, object_id:, id:, xact_id:, created:)
|
|
18
|
+
{
|
|
19
|
+
"object_type" => object_type,
|
|
20
|
+
"object_id" => object_id,
|
|
21
|
+
"id" => id,
|
|
22
|
+
"_xact_id" => xact_id,
|
|
23
|
+
"created" => created
|
|
24
|
+
}
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/braintrust/scorer.rb
CHANGED
|
@@ -152,10 +152,10 @@ module Braintrust
|
|
|
152
152
|
case block.arity
|
|
153
153
|
when 3
|
|
154
154
|
Log.warn_once(:scorer_positional_3, "Scorer with positional params (input, expected, output) is deprecated. Use keyword args: |input:, expected:, output:| instead.")
|
|
155
|
-
->(
|
|
155
|
+
->(input: nil, expected: nil, output: nil) { block.call(input, expected, output) }
|
|
156
156
|
when 4, -4, -1
|
|
157
157
|
Log.warn_once(:scorer_positional_4, "Scorer with positional params (input, expected, output, metadata) is deprecated. Use keyword args: |input:, expected:, output:, metadata:| instead.")
|
|
158
|
-
->(
|
|
158
|
+
->(input: nil, expected: nil, output: nil, metadata: nil) { block.call(input, expected, output, metadata) }
|
|
159
159
|
else
|
|
160
160
|
raise ArgumentError, "Scorer must accept keyword args or 3-4 positional params (got arity #{block.arity})"
|
|
161
161
|
end
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
+
require_relative "../../eval/case"
|
|
6
|
+
|
|
5
7
|
module Braintrust
|
|
6
8
|
module Server
|
|
7
9
|
module Services
|
|
8
10
|
# Framework-agnostic service for running evaluations and streaming SSE results.
|
|
9
11
|
# Must be long-lived (not per-request) to preserve the @state_cache across requests.
|
|
10
12
|
class Eval
|
|
13
|
+
# Row fields this SDK understands, derived from the Case contract so that
|
|
14
|
+
# adding a field there carries it through here. Anything else on an
|
|
15
|
+
# inbound row is a field this version has no code for, and is ignored.
|
|
16
|
+
CASE_FIELDS = Braintrust::Eval::Case.members.map(&:to_s).freeze
|
|
17
|
+
|
|
11
18
|
def initialize(evaluators)
|
|
12
19
|
@evaluators = evaluators
|
|
13
20
|
@state_mutex = Mutex.new
|
|
@@ -177,9 +184,10 @@ module Braintrust
|
|
|
177
184
|
# Returns [cases, dataset] where exactly one is non-nil.
|
|
178
185
|
def resolve_data_source(data)
|
|
179
186
|
if data.key?("data")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
# Rows arrive inline from the Playground carrying tags, metadata and
|
|
188
|
+
# an origin pointer back to the row they came from. Carry them all:
|
|
189
|
+
# the Playground matches streamed results to its grid by origin.
|
|
190
|
+
cases = data["data"].map { |row| row.slice(*CASE_FIELDS).transform_keys(&:to_sym) }
|
|
183
191
|
[cases, nil]
|
|
184
192
|
elsif data.key?("dataset_id")
|
|
185
193
|
[nil, Braintrust::Dataset::ID.new(id: data["dataset_id"])]
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Braintrust
|
|
@@ -118,6 +118,7 @@ files:
|
|
|
118
118
|
- lib/braintrust/contrib/rails/server/health_controller.rb
|
|
119
119
|
- lib/braintrust/contrib/rails/server/list_controller.rb
|
|
120
120
|
- lib/braintrust/contrib/rails/server/routes.rb
|
|
121
|
+
- lib/braintrust/contrib/rails/server/templates/initializer.rb.tt
|
|
121
122
|
- lib/braintrust/contrib/registry.rb
|
|
122
123
|
- lib/braintrust/contrib/ruby_llm/deprecated.rb
|
|
123
124
|
- lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb
|