langsmith-sdk 0.1.1
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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +120 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE +22 -0
- data/README.md +224 -0
- data/Rakefile +8 -0
- data/examples/LLM_TRACING.md +439 -0
- data/examples/complex_agent.rb +472 -0
- data/examples/llm_tracing.rb +304 -0
- data/examples/openai_integration.rb +751 -0
- data/langsmith.gemspec +38 -0
- data/lib/langsmith/batch_processor.rb +237 -0
- data/lib/langsmith/client.rb +181 -0
- data/lib/langsmith/configuration.rb +96 -0
- data/lib/langsmith/context.rb +73 -0
- data/lib/langsmith/errors.rb +13 -0
- data/lib/langsmith/railtie.rb +86 -0
- data/lib/langsmith/run.rb +320 -0
- data/lib/langsmith/run_tree.rb +154 -0
- data/lib/langsmith/traceable.rb +120 -0
- data/lib/langsmith/version.rb +5 -0
- data/lib/langsmith.rb +144 -0
- metadata +134 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langsmith
|
|
4
|
+
# Rails integration for automatic configuration and lifecycle management.
|
|
5
|
+
#
|
|
6
|
+
# When Rails is detected, this Railtie will:
|
|
7
|
+
# - Automatically configure Langsmith from Rails credentials or environment
|
|
8
|
+
# - Register shutdown hooks to flush traces before the application exits
|
|
9
|
+
# - Provide a generator for creating an initializer
|
|
10
|
+
#
|
|
11
|
+
# @example Using Rails credentials (config/credentials.yml.enc)
|
|
12
|
+
# langsmith:
|
|
13
|
+
# api_key: ls_...
|
|
14
|
+
# project: my-rails-app
|
|
15
|
+
#
|
|
16
|
+
# @example Using environment variables
|
|
17
|
+
# LANGSMITH_API_KEY=ls_...
|
|
18
|
+
# LANGSMITH_PROJECT=my-rails-app
|
|
19
|
+
# LANGSMITH_TRACING=true
|
|
20
|
+
#
|
|
21
|
+
class Railtie < ::Rails::Railtie
|
|
22
|
+
config.langsmith = ActiveSupport::OrderedOptions.new
|
|
23
|
+
|
|
24
|
+
# Default configuration values
|
|
25
|
+
config.langsmith.auto_configure = true
|
|
26
|
+
config.langsmith.tracing_enabled = nil # nil means defer to env var
|
|
27
|
+
|
|
28
|
+
initializer "langsmith.configure" do |app|
|
|
29
|
+
next unless app.config.langsmith.auto_configure
|
|
30
|
+
|
|
31
|
+
configure_from_rails(app)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config.after_initialize do
|
|
35
|
+
# Log configuration status in development
|
|
36
|
+
if Rails.env.development? && Langsmith.tracing_enabled?
|
|
37
|
+
Rails.logger.info "[Langsmith] Tracing enabled for project: #{Langsmith.configuration.project}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Ensure traces are flushed before the application exits
|
|
42
|
+
config.before_configuration do
|
|
43
|
+
at_exit do
|
|
44
|
+
Langsmith.shutdown if Langsmith.tracing_enabled?
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Rails.logger.error "[Langsmith] Error during shutdown: #{e.message}" if defined?(Rails.logger)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def configure_from_rails(app) # rubocop:disable Metrics/AbcSize
|
|
53
|
+
Langsmith.configure do |config|
|
|
54
|
+
# Try Rails credentials first, fall back to environment variables
|
|
55
|
+
credentials = app.credentials.langsmith || {}
|
|
56
|
+
|
|
57
|
+
config.api_key = credentials[:api_key] || ENV.fetch("LANGSMITH_API_KEY", nil)
|
|
58
|
+
config.endpoint = credentials[:endpoint] || ENV.fetch("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
|
|
59
|
+
config.project = credentials[:project] || ENV.fetch("LANGSMITH_PROJECT",
|
|
60
|
+
Rails.application.class.module_parent_name.underscore)
|
|
61
|
+
config.tenant_id = credentials[:tenant_id] || ENV.fetch("LANGSMITH_TENANT_ID", nil)
|
|
62
|
+
|
|
63
|
+
# Tracing can be set via Rails config, credentials, or environment
|
|
64
|
+
config.tracing_enabled = resolve_tracing_enabled(app, credentials)
|
|
65
|
+
|
|
66
|
+
# Optional settings from credentials
|
|
67
|
+
config.batch_size = credentials[:batch_size] if credentials[:batch_size]
|
|
68
|
+
config.flush_interval = credentials[:flush_interval] if credentials[:flush_interval]
|
|
69
|
+
config.timeout = credentials[:timeout] if credentials[:timeout]
|
|
70
|
+
end
|
|
71
|
+
rescue Langsmith::ConfigurationError => e
|
|
72
|
+
Rails.logger.warn "[Langsmith] Configuration error: #{e.message}" if defined?(Rails.logger)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resolve_tracing_enabled(app, credentials)
|
|
76
|
+
# Priority: Rails config > credentials > environment variable
|
|
77
|
+
return app.config.langsmith.tracing_enabled unless app.config.langsmith.tracing_enabled.nil?
|
|
78
|
+
return credentials[:tracing_enabled] unless credentials[:tracing_enabled].nil?
|
|
79
|
+
|
|
80
|
+
env_value = ENV.fetch("LANGSMITH_TRACING", nil)
|
|
81
|
+
return false if env_value.nil?
|
|
82
|
+
|
|
83
|
+
%w[true 1 yes on].include?(env_value.downcase)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Langsmith
|
|
8
|
+
# Represents a single trace run/span in LangSmith.
|
|
9
|
+
# All run types (chain, llm, tool, etc.) use this same class with different run_type values.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a run
|
|
12
|
+
# run = Langsmith::Run.new(name: "my_operation", run_type: "chain")
|
|
13
|
+
# run.add_metadata(user_id: "123")
|
|
14
|
+
# run.finish(outputs: { result: "success" })
|
|
15
|
+
class Run
|
|
16
|
+
# Valid run types supported by LangSmith
|
|
17
|
+
VALID_RUN_TYPES = %w[chain llm tool retriever prompt parser].freeze
|
|
18
|
+
|
|
19
|
+
# @return [String] unique identifier for this run
|
|
20
|
+
attr_reader :id
|
|
21
|
+
|
|
22
|
+
# @return [String] name of the operation
|
|
23
|
+
attr_reader :name
|
|
24
|
+
|
|
25
|
+
# @return [String] type of run (chain, llm, tool, etc.)
|
|
26
|
+
attr_reader :run_type
|
|
27
|
+
|
|
28
|
+
# @return [String, nil] parent run ID for nested traces
|
|
29
|
+
attr_reader :parent_run_id
|
|
30
|
+
|
|
31
|
+
# @return [String] project/session name
|
|
32
|
+
attr_reader :session_name
|
|
33
|
+
|
|
34
|
+
# @return [Time] when the run started
|
|
35
|
+
attr_reader :start_time
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] tenant ID for multi-tenant scenarios
|
|
38
|
+
attr_reader :tenant_id
|
|
39
|
+
|
|
40
|
+
# @return [String] trace ID (root run's ID)
|
|
41
|
+
attr_reader :trace_id
|
|
42
|
+
|
|
43
|
+
# @return [String] dotted order for trace tree ordering
|
|
44
|
+
attr_reader :dotted_order
|
|
45
|
+
|
|
46
|
+
# @return [Hash] input data
|
|
47
|
+
attr_accessor :inputs
|
|
48
|
+
|
|
49
|
+
# @return [Hash, nil] output data
|
|
50
|
+
attr_accessor :outputs
|
|
51
|
+
|
|
52
|
+
# @return [String, nil] error message if run failed
|
|
53
|
+
attr_accessor :error
|
|
54
|
+
|
|
55
|
+
# @return [Time, nil] when the run ended
|
|
56
|
+
attr_accessor :end_time
|
|
57
|
+
|
|
58
|
+
# @return [Hash] additional metadata
|
|
59
|
+
attr_accessor :metadata
|
|
60
|
+
|
|
61
|
+
# @return [Hash] extra data (e.g., token usage)
|
|
62
|
+
attr_accessor :extra
|
|
63
|
+
|
|
64
|
+
# @return [Array<Hash>] events that occurred during the run
|
|
65
|
+
attr_accessor :events
|
|
66
|
+
|
|
67
|
+
# @return [Array<String>] tags for filtering
|
|
68
|
+
attr_accessor :tags
|
|
69
|
+
|
|
70
|
+
# Creates a new Run instance.
|
|
71
|
+
#
|
|
72
|
+
# @param name [String] name of the operation
|
|
73
|
+
# @param run_type [String] type of run ("chain", "llm", "tool", etc.)
|
|
74
|
+
# @param inputs [Hash, nil] input data
|
|
75
|
+
# @param parent_run_id [String, nil] parent run ID for nested traces
|
|
76
|
+
# @param session_name [String, nil] project/session name
|
|
77
|
+
# @param metadata [Hash, nil] additional metadata
|
|
78
|
+
# @param tags [Array<String>, nil] tags for filtering
|
|
79
|
+
# @param extra [Hash, nil] extra data
|
|
80
|
+
# @param id [String, nil] custom ID (auto-generated if not provided)
|
|
81
|
+
# @param tenant_id [String, nil] tenant ID for multi-tenant scenarios
|
|
82
|
+
# @param trace_id [String, nil] trace ID (defaults to own ID for root runs)
|
|
83
|
+
# @param parent_dotted_order [String, nil] parent's dotted order for tree ordering
|
|
84
|
+
#
|
|
85
|
+
# @raise [ArgumentError] if run_type is invalid
|
|
86
|
+
def initialize(
|
|
87
|
+
name:,
|
|
88
|
+
run_type: "chain",
|
|
89
|
+
inputs: nil,
|
|
90
|
+
parent_run_id: nil,
|
|
91
|
+
session_name: nil,
|
|
92
|
+
metadata: nil,
|
|
93
|
+
tags: nil,
|
|
94
|
+
extra: nil,
|
|
95
|
+
id: nil,
|
|
96
|
+
tenant_id: nil,
|
|
97
|
+
trace_id: nil,
|
|
98
|
+
parent_dotted_order: nil
|
|
99
|
+
)
|
|
100
|
+
@id = id || SecureRandom.uuid
|
|
101
|
+
@name = name
|
|
102
|
+
@run_type = validate_run_type(run_type)
|
|
103
|
+
@inputs = inputs || {}
|
|
104
|
+
@outputs = nil
|
|
105
|
+
@error = nil
|
|
106
|
+
@parent_run_id = parent_run_id
|
|
107
|
+
@session_name = session_name || Langsmith.configuration.project
|
|
108
|
+
@tenant_id = tenant_id || Langsmith.configuration.tenant_id
|
|
109
|
+
# trace_id is the root run's ID; for root runs it equals the run's own ID
|
|
110
|
+
@trace_id = trace_id || @id
|
|
111
|
+
@start_time = Time.now.utc
|
|
112
|
+
@end_time = nil
|
|
113
|
+
@metadata = metadata || {}
|
|
114
|
+
@tags = tags || []
|
|
115
|
+
@extra = extra || {}
|
|
116
|
+
@events = []
|
|
117
|
+
# dotted_order is used for ordering runs in the trace tree
|
|
118
|
+
@dotted_order = build_dotted_order(parent_dotted_order)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Marks the run as finished.
|
|
122
|
+
#
|
|
123
|
+
# @param outputs [Hash, nil] output data
|
|
124
|
+
# @param error [Exception, String, nil] error if the run failed
|
|
125
|
+
# @return [self]
|
|
126
|
+
def finish(outputs: nil, error: nil)
|
|
127
|
+
@end_time = Time.now.utc
|
|
128
|
+
@outputs = outputs if outputs
|
|
129
|
+
@error = format_error(error) if error
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Adds metadata to the run.
|
|
134
|
+
#
|
|
135
|
+
# @param new_metadata [Hash] metadata to merge
|
|
136
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
137
|
+
def add_metadata(new_metadata)
|
|
138
|
+
@metadata.merge!(new_metadata)
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Adds tags to the run.
|
|
143
|
+
#
|
|
144
|
+
# @param new_tags [Array<String>] tags to add
|
|
145
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
146
|
+
def add_tags(*new_tags)
|
|
147
|
+
@tags.concat(new_tags.flatten)
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Adds an event to the run.
|
|
152
|
+
#
|
|
153
|
+
# @param name [String] event name
|
|
154
|
+
# @param time [Time, nil] event time (defaults to now)
|
|
155
|
+
# @param kwargs [Hash] additional event data
|
|
156
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
157
|
+
def add_event(name:, time: nil, **kwargs)
|
|
158
|
+
@events << {
|
|
159
|
+
name: name,
|
|
160
|
+
time: (time || Time.now.utc).iso8601(3),
|
|
161
|
+
**kwargs
|
|
162
|
+
}
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Sets token usage for LLM runs.
|
|
167
|
+
# Follows the Python SDK pattern: tokens are stored in extra.metadata.usage_metadata
|
|
168
|
+
# with keys: input_tokens, output_tokens, total_tokens
|
|
169
|
+
#
|
|
170
|
+
# @param input_tokens [Integer, nil] number of input/prompt tokens
|
|
171
|
+
# @param output_tokens [Integer, nil] number of output/completion tokens
|
|
172
|
+
# @param total_tokens [Integer, nil] total tokens (calculated if not provided)
|
|
173
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
174
|
+
def set_token_usage(input_tokens: nil, output_tokens: nil, total_tokens: nil)
|
|
175
|
+
calculated_total = total_tokens || ((input_tokens || 0) + (output_tokens || 0))
|
|
176
|
+
|
|
177
|
+
@extra[:metadata] ||= {}
|
|
178
|
+
@extra[:metadata][:usage_metadata] = {
|
|
179
|
+
input_tokens: input_tokens,
|
|
180
|
+
output_tokens: output_tokens,
|
|
181
|
+
total_tokens: calculated_total
|
|
182
|
+
}.compact
|
|
183
|
+
|
|
184
|
+
nil # Return nil to prevent circular reference if used as last line of trace block
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Sets LLM model metadata.
|
|
188
|
+
# The model name should be stored in extra.metadata for LangSmith to display it.
|
|
189
|
+
#
|
|
190
|
+
# @param model [String] the model name/identifier
|
|
191
|
+
# @param provider [String, nil] the model provider (e.g., "openai", "anthropic")
|
|
192
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
193
|
+
def set_model(model:, provider: nil)
|
|
194
|
+
@extra[:metadata] ||= {}
|
|
195
|
+
@extra[:metadata][:ls_model_name] = model
|
|
196
|
+
@extra[:metadata][:ls_provider] = provider if provider
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Sets streaming metrics for LLM runs.
|
|
201
|
+
# Useful for tracking performance of streaming responses.
|
|
202
|
+
#
|
|
203
|
+
# @param time_to_first_token [Float, nil] time in seconds until first token received
|
|
204
|
+
# @param chunk_count [Integer, nil] total number of chunks received
|
|
205
|
+
# @param tokens_per_second [Float, nil] throughput in tokens per second
|
|
206
|
+
# @return [nil] returns nil to prevent circular reference when used as last line
|
|
207
|
+
def set_streaming_metrics(time_to_first_token: nil, chunk_count: nil, tokens_per_second: nil)
|
|
208
|
+
@extra[:metadata] ||= {}
|
|
209
|
+
@extra[:metadata][:streaming_metrics] = {
|
|
210
|
+
time_to_first_token_s: time_to_first_token,
|
|
211
|
+
chunk_count: chunk_count,
|
|
212
|
+
tokens_per_second: tokens_per_second
|
|
213
|
+
}.compact
|
|
214
|
+
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns whether the run has finished.
|
|
219
|
+
# @return [Boolean]
|
|
220
|
+
def finished?
|
|
221
|
+
!end_time.nil?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns the duration in milliseconds.
|
|
225
|
+
# @return [Float, nil] duration in ms, or nil if not finished
|
|
226
|
+
def duration_ms
|
|
227
|
+
return nil unless end_time
|
|
228
|
+
|
|
229
|
+
((end_time - start_time) * 1000).round(2)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Convert to hash for JSON serialization to LangSmith API (full run for POST).
|
|
233
|
+
# Token usage is stored in extra.metadata.usage_metadata following Python SDK pattern.
|
|
234
|
+
#
|
|
235
|
+
# @return [Hash]
|
|
236
|
+
def to_h
|
|
237
|
+
{
|
|
238
|
+
id:,
|
|
239
|
+
name:,
|
|
240
|
+
run_type:,
|
|
241
|
+
inputs:,
|
|
242
|
+
outputs:,
|
|
243
|
+
error:,
|
|
244
|
+
parent_run_id:,
|
|
245
|
+
trace_id:,
|
|
246
|
+
dotted_order:,
|
|
247
|
+
session_name:,
|
|
248
|
+
start_time: start_time.iso8601(3),
|
|
249
|
+
end_time: end_time&.iso8601(3),
|
|
250
|
+
extra: extra.empty? ? nil : extra,
|
|
251
|
+
events: events.empty? ? nil : events,
|
|
252
|
+
tags: tags.empty? ? nil : tags,
|
|
253
|
+
serialized: { name: },
|
|
254
|
+
**(metadata.empty? ? {} : { metadata: })
|
|
255
|
+
}.compact
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Convert to hash for PATCH requests (only fields that change on completion).
|
|
259
|
+
# Note: parent_run_id is required for LangSmith to validate dotted_order correctly.
|
|
260
|
+
# Token usage is included in extra.metadata.usage_metadata.
|
|
261
|
+
# Metadata and tags are included as they may be added during execution.
|
|
262
|
+
#
|
|
263
|
+
# @return [Hash]
|
|
264
|
+
def to_update_h
|
|
265
|
+
{
|
|
266
|
+
id:,
|
|
267
|
+
trace_id:,
|
|
268
|
+
parent_run_id:,
|
|
269
|
+
dotted_order:,
|
|
270
|
+
end_time: end_time&.iso8601(3),
|
|
271
|
+
outputs:,
|
|
272
|
+
error:,
|
|
273
|
+
events: events.empty? ? nil : events,
|
|
274
|
+
extra: extra.empty? ? nil : extra,
|
|
275
|
+
tags: tags.empty? ? nil : tags,
|
|
276
|
+
**(metadata.empty? ? {} : { metadata: })
|
|
277
|
+
}.compact
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Convert to JSON string.
|
|
281
|
+
#
|
|
282
|
+
# @return [String]
|
|
283
|
+
def to_json(*args)
|
|
284
|
+
to_h.to_json(*args)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
def validate_run_type(run_type)
|
|
290
|
+
return run_type if VALID_RUN_TYPES.include?(run_type)
|
|
291
|
+
|
|
292
|
+
raise ArgumentError, "Invalid run_type '#{run_type}'. Must be one of: #{VALID_RUN_TYPES.join(", ")}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def format_error(error)
|
|
296
|
+
case error
|
|
297
|
+
when Exception
|
|
298
|
+
"#{error.class}: #{error.message}\n#{error.backtrace&.first(10)&.join("\n")}"
|
|
299
|
+
when String
|
|
300
|
+
error
|
|
301
|
+
else
|
|
302
|
+
error.to_s
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Build the dotted_order string for trace ordering
|
|
307
|
+
# Format: {timestamp}{id} for root, {parent_dotted_order}.{timestamp}{id} for children
|
|
308
|
+
def build_dotted_order(parent_dotted_order)
|
|
309
|
+
# Format timestamp as YYYYMMDDTHHMMSSffffffZ (compact ISO8601 with microseconds)
|
|
310
|
+
timestamp = @start_time.strftime("%Y%m%dT%H%M%S%6NZ")
|
|
311
|
+
order_part = "#{timestamp}#{@id}"
|
|
312
|
+
|
|
313
|
+
if parent_dotted_order
|
|
314
|
+
"#{parent_dotted_order}.#{order_part}"
|
|
315
|
+
else
|
|
316
|
+
order_part
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run"
|
|
4
|
+
require_relative "context"
|
|
5
|
+
|
|
6
|
+
module Langsmith
|
|
7
|
+
# RunTree manages the creation and lifecycle of trace runs.
|
|
8
|
+
# It handles parent-child relationships and coordinates with the batch processor.
|
|
9
|
+
class RunTree
|
|
10
|
+
attr_reader :run
|
|
11
|
+
|
|
12
|
+
def initialize(
|
|
13
|
+
name:,
|
|
14
|
+
run_type: "chain",
|
|
15
|
+
inputs: nil,
|
|
16
|
+
metadata: nil,
|
|
17
|
+
tags: nil,
|
|
18
|
+
extra: nil,
|
|
19
|
+
parent_run_id: nil,
|
|
20
|
+
tenant_id: nil,
|
|
21
|
+
project: nil
|
|
22
|
+
)
|
|
23
|
+
# If no explicit parent, check context for current parent
|
|
24
|
+
effective_parent_id = parent_run_id || Context.current_parent_run_id
|
|
25
|
+
|
|
26
|
+
# Inherit tenant_id from parent run if not explicitly set
|
|
27
|
+
effective_tenant_id = tenant_id || Context.current_run&.tenant_id
|
|
28
|
+
|
|
29
|
+
# Child traces must use the same project as their parent to keep the trace tree together.
|
|
30
|
+
# Only root traces can set the project; children always inherit from parent.
|
|
31
|
+
effective_project = Context.current_run&.session_name || project
|
|
32
|
+
|
|
33
|
+
# Inherit trace_id from root run (parent's trace_id)
|
|
34
|
+
# For root runs, trace_id will default to the run's own ID
|
|
35
|
+
effective_trace_id = Context.current_run&.trace_id
|
|
36
|
+
|
|
37
|
+
# Inherit dotted_order from parent for proper trace ordering
|
|
38
|
+
parent_dotted_order = Context.current_run&.dotted_order
|
|
39
|
+
|
|
40
|
+
@run = Run.new(
|
|
41
|
+
name: name,
|
|
42
|
+
run_type: run_type,
|
|
43
|
+
inputs: inputs,
|
|
44
|
+
parent_run_id: effective_parent_id,
|
|
45
|
+
metadata: metadata,
|
|
46
|
+
tags: tags,
|
|
47
|
+
extra: extra,
|
|
48
|
+
tenant_id: effective_tenant_id,
|
|
49
|
+
session_name: effective_project,
|
|
50
|
+
trace_id: effective_trace_id,
|
|
51
|
+
parent_dotted_order: parent_dotted_order
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@posted_start = false
|
|
55
|
+
@posted_end = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Post the run start to LangSmith
|
|
59
|
+
def post_start
|
|
60
|
+
return if @posted_start || !Langsmith.tracing_enabled?
|
|
61
|
+
|
|
62
|
+
Langsmith.batch_processor.enqueue_create(@run)
|
|
63
|
+
@posted_start = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Post the run end to LangSmith
|
|
67
|
+
def post_end
|
|
68
|
+
return if @posted_end || !Langsmith.tracing_enabled?
|
|
69
|
+
|
|
70
|
+
Langsmith.batch_processor.enqueue_update(@run)
|
|
71
|
+
@posted_end = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Execute a block within this run's context
|
|
75
|
+
def execute
|
|
76
|
+
return yield(@run) unless Langsmith.tracing_enabled?
|
|
77
|
+
|
|
78
|
+
post_start
|
|
79
|
+
|
|
80
|
+
Context.with_run(@run) do
|
|
81
|
+
result = yield(@run)
|
|
82
|
+
@run.finish(outputs: sanitize_outputs(result))
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
@run.finish(error: e)
|
|
87
|
+
raise
|
|
88
|
+
ensure
|
|
89
|
+
post_end if Langsmith.tracing_enabled?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convenience methods that delegate to the run
|
|
93
|
+
def add_metadata(...)
|
|
94
|
+
@run.add_metadata(...)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def add_tags(...)
|
|
98
|
+
@run.add_tags(...)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set_inputs(inputs)
|
|
102
|
+
@run.inputs = inputs
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def set_outputs(outputs)
|
|
106
|
+
@run.outputs = outputs
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def set_token_usage(...)
|
|
110
|
+
@run.set_token_usage(...)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def set_model(...)
|
|
114
|
+
@run.set_model(...)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def set_streaming_metrics(...)
|
|
118
|
+
@run.set_streaming_metrics(...)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def id
|
|
122
|
+
@run.id
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parent_run_id
|
|
126
|
+
@run.parent_run_id
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Create a child run tree
|
|
130
|
+
def create_child(name:, run_type: "chain", **kwargs)
|
|
131
|
+
RunTree.new(
|
|
132
|
+
name: name,
|
|
133
|
+
run_type: run_type,
|
|
134
|
+
parent_run_id: @run.id,
|
|
135
|
+
**kwargs
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# Sanitize block results to prevent circular references.
|
|
142
|
+
# When users call methods like `run.add_metadata(...)` as the last line,
|
|
143
|
+
# the Run object itself becomes the result, creating a circular reference.
|
|
144
|
+
def sanitize_outputs(result)
|
|
145
|
+
case result
|
|
146
|
+
when Run, RunTree, nil
|
|
147
|
+
# Run/RunTree objects would create circular reference, nil means no output
|
|
148
|
+
nil
|
|
149
|
+
else
|
|
150
|
+
{ result: result }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langsmith
|
|
4
|
+
# Module that provides method decoration for automatic tracing.
|
|
5
|
+
# Include this module in your class and use the `traceable` class method
|
|
6
|
+
# to mark methods for tracing.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# class MyService
|
|
10
|
+
# include Langsmith::Traceable
|
|
11
|
+
#
|
|
12
|
+
# traceable run_type: "llm"
|
|
13
|
+
# def call_llm(prompt)
|
|
14
|
+
# # automatically traced
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# traceable run_type: "tool", name: "search"
|
|
18
|
+
# def search(query)
|
|
19
|
+
# # traced with custom name
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# traceable run_type: "chain", tenant_id: "tenant-123"
|
|
23
|
+
# def process_for_tenant(data)
|
|
24
|
+
# # traced to specific tenant
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
module Traceable
|
|
28
|
+
def self.included(base)
|
|
29
|
+
base.extend(ClassMethods)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ClassMethods
|
|
33
|
+
# Marks the next defined method as traceable
|
|
34
|
+
def traceable(run_type: "chain", name: nil, metadata: nil, tags: nil, tenant_id: nil)
|
|
35
|
+
@pending_traceable_options = {
|
|
36
|
+
run_type: run_type,
|
|
37
|
+
name: name,
|
|
38
|
+
metadata: metadata,
|
|
39
|
+
tags: tags,
|
|
40
|
+
tenant_id: tenant_id
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def method_added(method_name)
|
|
45
|
+
super
|
|
46
|
+
|
|
47
|
+
return unless @pending_traceable_options
|
|
48
|
+
|
|
49
|
+
options = @pending_traceable_options
|
|
50
|
+
@pending_traceable_options = nil
|
|
51
|
+
|
|
52
|
+
# Don't wrap private/protected methods that start with underscore
|
|
53
|
+
return if method_name.to_s.start_with?("_langsmith_")
|
|
54
|
+
|
|
55
|
+
wrap_method(method_name, options)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def wrap_method(method_name, options)
|
|
61
|
+
original_method = instance_method(method_name)
|
|
62
|
+
trace_name = options[:name] || "#{name}##{method_name}"
|
|
63
|
+
|
|
64
|
+
# Remove original method to avoid "method redefined" warning
|
|
65
|
+
remove_method(method_name)
|
|
66
|
+
|
|
67
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
68
|
+
Langsmith.trace(
|
|
69
|
+
trace_name,
|
|
70
|
+
run_type: options[:run_type],
|
|
71
|
+
inputs: build_trace_inputs(args, kwargs, original_method),
|
|
72
|
+
metadata: options[:metadata],
|
|
73
|
+
tags: options[:tags],
|
|
74
|
+
tenant_id: options[:tenant_id]
|
|
75
|
+
) do |_run|
|
|
76
|
+
if kwargs.empty?
|
|
77
|
+
original_method.bind(self).call(*args, &block)
|
|
78
|
+
else
|
|
79
|
+
original_method.bind(self).call(*args, **kwargs, &block)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def build_trace_inputs(args, kwargs, method)
|
|
89
|
+
params = method.parameters
|
|
90
|
+
inputs = {}
|
|
91
|
+
|
|
92
|
+
# Map positional arguments
|
|
93
|
+
args.each_with_index do |arg, index|
|
|
94
|
+
param = params[index]
|
|
95
|
+
param_name = param ? param[1] : "arg#{index}"
|
|
96
|
+
inputs[param_name] = serialize_input(arg)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Map keyword arguments
|
|
100
|
+
kwargs.each do |key, value|
|
|
101
|
+
inputs[key] = serialize_input(value)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
inputs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def serialize_input(value)
|
|
108
|
+
case value
|
|
109
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
110
|
+
value
|
|
111
|
+
when Array
|
|
112
|
+
value.map { |v| serialize_input(v) }
|
|
113
|
+
when Hash
|
|
114
|
+
value.transform_values { |v| serialize_input(v) }
|
|
115
|
+
else
|
|
116
|
+
value.to_s
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|