braintrust 0.4.2 → 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
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
|
|
|
@@ -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
|
|
|
@@ -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
|
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
|
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
|