braintrust 0.0.6 → 0.0.7
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/trace/contrib/github.com/crmne/ruby_llm.rb +141 -33
- data/lib/braintrust/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ad055b60c4efb984bce955b1c00c684c760c849dea7a54d49a452f925dbab629
|
|
4
|
+
data.tar.gz: fe271abfac7810e53ff88efb139bc41b519799ed7dbd196ab5bb64fcbc35b62c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a33fc58073542bf7d7dbf45092d9f3a6669d7f13fa98d3d8753d8fccd456c7e010b8a3ac0be5625e6761e164d2223f20b7fbaa3f02ead978f47398153c6c8ac2
|
|
7
|
+
data.tar.gz: b24f1377f4ec25f09c7c5e1366e1428c9164d7b3b7bdcf82331aa84d10603b250c61983ea811a13e7be1a1276558ef2995292fbefb20e99a374e59e9eaefb8b1
|
|
@@ -71,15 +71,59 @@ module Braintrust
|
|
|
71
71
|
# Check if already wrapped to make this idempotent
|
|
72
72
|
return chat if chat.instance_variable_get(:@braintrust_wrapped)
|
|
73
73
|
|
|
74
|
-
# Create a wrapper module that intercepts chat.
|
|
74
|
+
# Create a wrapper module that intercepts chat.complete
|
|
75
75
|
wrapper = create_wrapper_module(tracer_provider)
|
|
76
76
|
|
|
77
77
|
# Mark as wrapped and prepend the wrapper to the chat instance
|
|
78
78
|
chat.instance_variable_set(:@braintrust_wrapped, true)
|
|
79
79
|
chat.singleton_class.prepend(wrapper)
|
|
80
|
+
|
|
81
|
+
# Register tool callbacks for tool span creation
|
|
82
|
+
register_tool_callbacks(chat, tracer_provider)
|
|
83
|
+
|
|
80
84
|
chat
|
|
81
85
|
end
|
|
82
86
|
|
|
87
|
+
# Register callbacks for tool execution tracing
|
|
88
|
+
# @param chat [RubyLLM::Chat] the chat instance
|
|
89
|
+
# @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider
|
|
90
|
+
def self.register_tool_callbacks(chat, tracer_provider)
|
|
91
|
+
tracer = tracer_provider.tracer("braintrust")
|
|
92
|
+
|
|
93
|
+
# Track tool spans by tool_call_id
|
|
94
|
+
tool_spans = {}
|
|
95
|
+
|
|
96
|
+
# Start tool span when tool is called
|
|
97
|
+
chat.on_tool_call do |tool_call|
|
|
98
|
+
span = tracer.start_span("ruby_llm.tool.#{tool_call.name}")
|
|
99
|
+
set_json_attr(span, "braintrust.span_attributes", {type: "tool"})
|
|
100
|
+
span.set_attribute("tool.name", tool_call.name)
|
|
101
|
+
span.set_attribute("tool.call_id", tool_call.id)
|
|
102
|
+
|
|
103
|
+
# Store tool input
|
|
104
|
+
input = {
|
|
105
|
+
"name" => tool_call.name,
|
|
106
|
+
"arguments" => tool_call.arguments
|
|
107
|
+
}
|
|
108
|
+
set_json_attr(span, "braintrust.input_json", input)
|
|
109
|
+
|
|
110
|
+
tool_spans[tool_call.id] = span
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# End tool span when result is received
|
|
114
|
+
chat.on_tool_result do |result|
|
|
115
|
+
# Find the most recent tool span (RubyLLM doesn't pass tool_call_id to on_tool_result)
|
|
116
|
+
# The spans are processed in order, so we can use the first unfinished one
|
|
117
|
+
tool_call_id, span = tool_spans.find { |_id, s| s }
|
|
118
|
+
if span
|
|
119
|
+
# Store tool output
|
|
120
|
+
set_json_attr(span, "braintrust.output_json", result)
|
|
121
|
+
span.finish
|
|
122
|
+
tool_spans.delete(tool_call_id)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
83
127
|
# Unwrap RubyLLM to remove Braintrust tracing
|
|
84
128
|
# For class-level unwrapping, removes the initialize override from the wrapper module
|
|
85
129
|
# For instance-level unwrapping, clears the wrapped flag
|
|
@@ -116,50 +160,75 @@ module Braintrust
|
|
|
116
160
|
::RubyLLM::Chat.prepend(wrapper)
|
|
117
161
|
end
|
|
118
162
|
|
|
119
|
-
# Create the wrapper module that intercepts chat.
|
|
163
|
+
# Create the wrapper module that intercepts chat.complete
|
|
164
|
+
# We wrap complete() instead of ask() because:
|
|
165
|
+
# - ask() internally calls complete() for the actual API call
|
|
166
|
+
# - ActiveRecord integration (acts_as_chat) calls complete() directly
|
|
167
|
+
# - This ensures all LLM calls are traced regardless of entry point
|
|
168
|
+
#
|
|
169
|
+
# Important: RubyLLM's complete() calls itself recursively for tool execution.
|
|
170
|
+
# We only create a span for the outermost call to avoid duplicate spans.
|
|
171
|
+
# Tool execution is traced separately via on_tool_call/on_tool_result callbacks.
|
|
172
|
+
#
|
|
120
173
|
# @param tracer_provider [OpenTelemetry::SDK::Trace::TracerProvider] the tracer provider
|
|
121
174
|
# @return [Module] the wrapper module
|
|
122
175
|
def self.create_wrapper_module(tracer_provider)
|
|
123
176
|
Module.new do
|
|
124
|
-
define_method(:
|
|
177
|
+
define_method(:complete) do |&block|
|
|
178
|
+
# Check if we're already inside a traced complete() call
|
|
179
|
+
# If so, just call super without creating a new span
|
|
180
|
+
if @braintrust_in_complete
|
|
181
|
+
if block
|
|
182
|
+
return super(&block)
|
|
183
|
+
else
|
|
184
|
+
return super()
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
125
188
|
tracer = tracer_provider.tracer("braintrust")
|
|
126
189
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
# Mark that we're inside a complete() call
|
|
191
|
+
@braintrust_in_complete = true
|
|
192
|
+
|
|
193
|
+
begin
|
|
194
|
+
if block
|
|
195
|
+
# Handle streaming request
|
|
196
|
+
wrapped_block = proc do |chunk|
|
|
197
|
+
block.call(chunk)
|
|
198
|
+
end
|
|
199
|
+
Braintrust::Trace::Contrib::Github::Crmne::RubyLLM.handle_streaming_complete(self, tracer, block) do |aggregated_chunks|
|
|
200
|
+
super(&proc do |chunk|
|
|
201
|
+
aggregated_chunks << chunk
|
|
202
|
+
wrapped_block.call(chunk)
|
|
203
|
+
end)
|
|
204
|
+
end
|
|
205
|
+
else
|
|
206
|
+
# Handle non-streaming request
|
|
207
|
+
Braintrust::Trace::Contrib::Github::Crmne::RubyLLM.handle_non_streaming_complete(self, tracer) do
|
|
208
|
+
super()
|
|
136
209
|
end
|
|
137
210
|
end
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Braintrust::Trace::Contrib::Github::Crmne::RubyLLM.handle_non_streaming_ask(self, tracer, prompt, params) do
|
|
141
|
-
super(prompt, **params)
|
|
142
|
-
end
|
|
211
|
+
ensure
|
|
212
|
+
@braintrust_in_complete = false
|
|
143
213
|
end
|
|
144
214
|
end
|
|
145
215
|
end
|
|
146
216
|
end
|
|
147
217
|
|
|
148
|
-
# Handle streaming
|
|
218
|
+
# Handle streaming complete request with tracing
|
|
149
219
|
# @param chat [RubyLLM::Chat] the chat instance
|
|
150
220
|
# @param tracer [OpenTelemetry::Trace::Tracer] the tracer
|
|
151
|
-
# @param prompt [String, nil] the user prompt
|
|
152
|
-
# @param params [Hash] additional parameters
|
|
153
221
|
# @param block [Proc] the streaming block
|
|
154
|
-
def self.
|
|
222
|
+
def self.handle_streaming_complete(chat, tracer, block)
|
|
155
223
|
# Start span immediately for accurate timing
|
|
156
|
-
span = tracer.start_span("ruby_llm.chat
|
|
224
|
+
span = tracer.start_span("ruby_llm.chat")
|
|
157
225
|
|
|
158
226
|
aggregated_chunks = []
|
|
159
227
|
|
|
160
228
|
# Extract metadata and build input messages
|
|
229
|
+
# For complete(), messages are already in chat history (no prompt param)
|
|
161
230
|
metadata = extract_metadata(chat, stream: true)
|
|
162
|
-
input_messages = build_input_messages(chat,
|
|
231
|
+
input_messages = build_input_messages(chat, nil)
|
|
163
232
|
|
|
164
233
|
# Set input and metadata
|
|
165
234
|
set_json_attr(span, "braintrust.input_json", input_messages) if input_messages.any?
|
|
@@ -181,19 +250,18 @@ module Braintrust
|
|
|
181
250
|
result
|
|
182
251
|
end
|
|
183
252
|
|
|
184
|
-
# Handle non-streaming
|
|
253
|
+
# Handle non-streaming complete request with tracing
|
|
185
254
|
# @param chat [RubyLLM::Chat] the chat instance
|
|
186
255
|
# @param tracer [OpenTelemetry::Trace::Tracer] the tracer
|
|
187
|
-
|
|
188
|
-
# @param params [Hash] additional parameters
|
|
189
|
-
def self.handle_non_streaming_ask(chat, tracer, prompt, params)
|
|
256
|
+
def self.handle_non_streaming_complete(chat, tracer)
|
|
190
257
|
# Start span immediately for accurate timing
|
|
191
|
-
span = tracer.start_span("ruby_llm.chat
|
|
258
|
+
span = tracer.start_span("ruby_llm.chat")
|
|
192
259
|
|
|
193
260
|
begin
|
|
194
261
|
# Extract metadata and build input messages
|
|
262
|
+
# For complete(), messages are already in chat history (no prompt param)
|
|
195
263
|
metadata = extract_metadata(chat)
|
|
196
|
-
input_messages = build_input_messages(chat,
|
|
264
|
+
input_messages = build_input_messages(chat, nil)
|
|
197
265
|
set_json_attr(span, "braintrust.input_json", input_messages) if input_messages.any?
|
|
198
266
|
|
|
199
267
|
# Remember message count before the call (for tool call detection)
|
|
@@ -321,23 +389,62 @@ module Braintrust
|
|
|
321
389
|
end
|
|
322
390
|
|
|
323
391
|
# Build input messages array from chat history and prompt
|
|
392
|
+
# Formats messages to match OpenAI's message format
|
|
324
393
|
# @param chat [RubyLLM::Chat] the chat instance
|
|
325
394
|
# @param prompt [String, nil] the user prompt
|
|
326
395
|
# @return [Array<Hash>] array of message hashes
|
|
327
396
|
def self.build_input_messages(chat, prompt)
|
|
328
397
|
input_messages = []
|
|
329
398
|
|
|
330
|
-
# Add conversation history
|
|
399
|
+
# Add conversation history, formatting each message to OpenAI format
|
|
331
400
|
if chat.respond_to?(:messages) && chat.messages&.any?
|
|
332
|
-
input_messages = chat.messages.map { |m| m
|
|
401
|
+
input_messages = chat.messages.map { |m| format_message_for_input(m) }
|
|
333
402
|
end
|
|
334
403
|
|
|
335
404
|
# Add current prompt
|
|
336
|
-
input_messages << {role
|
|
405
|
+
input_messages << {"role" => "user", "content" => prompt} if prompt
|
|
337
406
|
|
|
338
407
|
input_messages
|
|
339
408
|
end
|
|
340
409
|
|
|
410
|
+
# Format a RubyLLM message to OpenAI-compatible format
|
|
411
|
+
# @param msg [Object] the RubyLLM message
|
|
412
|
+
# @return [Hash] OpenAI-formatted message
|
|
413
|
+
def self.format_message_for_input(msg)
|
|
414
|
+
formatted = {
|
|
415
|
+
"role" => msg.role.to_s
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# Handle content
|
|
419
|
+
if msg.respond_to?(:content) && msg.content
|
|
420
|
+
# Convert Ruby hash notation to JSON string for tool results
|
|
421
|
+
content = msg.content
|
|
422
|
+
if msg.role.to_s == "tool" && content.is_a?(String) && content.start_with?("{:")
|
|
423
|
+
# Ruby hash string like "{:location=>...}" - try to parse and re-serialize as JSON
|
|
424
|
+
begin
|
|
425
|
+
# Simple conversion: replace Ruby hash syntax with JSON
|
|
426
|
+
content = content.gsub(/(?<=\{|, ):(\w+)=>/, '"\1":').gsub("=>", ":")
|
|
427
|
+
rescue
|
|
428
|
+
# Keep original if conversion fails
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
formatted["content"] = content
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Handle tool_calls for assistant messages
|
|
435
|
+
if msg.respond_to?(:tool_calls) && msg.tool_calls&.any?
|
|
436
|
+
formatted["tool_calls"] = format_tool_calls(msg.tool_calls)
|
|
437
|
+
formatted["content"] = nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Handle tool_call_id for tool result messages
|
|
441
|
+
if msg.respond_to?(:tool_call_id) && msg.tool_call_id
|
|
442
|
+
formatted["tool_call_id"] = msg.tool_call_id
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
formatted
|
|
446
|
+
end
|
|
447
|
+
|
|
341
448
|
# Capture streaming output and metrics
|
|
342
449
|
# @param span [OpenTelemetry::Trace::Span] the span
|
|
343
450
|
# @param aggregated_chunks [Array] the aggregated chunks
|
|
@@ -383,8 +490,9 @@ module Braintrust
|
|
|
383
490
|
end
|
|
384
491
|
|
|
385
492
|
# Check if there are tool calls in the messages history
|
|
493
|
+
# Look at messages added during this complete() call
|
|
386
494
|
if chat.respond_to?(:messages) && chat.messages
|
|
387
|
-
assistant_msg = chat.messages[
|
|
495
|
+
assistant_msg = chat.messages[messages_before_count..].find { |m|
|
|
388
496
|
m.role.to_s == "assistant" && m.respond_to?(:tool_calls) && m.tool_calls&.any?
|
|
389
497
|
}
|
|
390
498
|
|
data/lib/braintrust/version.rb
CHANGED