langsmithrb_rails 0.1.0 → 0.3.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rspec_status +161 -0
  4. data/CHANGELOG.md +38 -0
  5. data/Gemfile +20 -0
  6. data/Gemfile.lock +321 -0
  7. data/LICENSE +21 -0
  8. data/README.md +421 -0
  9. data/Rakefile +10 -0
  10. data/langsmithrb_rails-0.1.0.gem +0 -0
  11. data/langsmithrb_rails-0.1.1.gem +0 -0
  12. data/langsmithrb_rails.gemspec +45 -0
  13. data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
  14. data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
  15. data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
  16. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
  17. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
  18. data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
  19. data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
  20. data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
  21. data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
  22. data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
  23. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
  24. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
  25. data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
  26. data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
  27. data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
  28. data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
  29. data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
  30. data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
  31. data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
  32. data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
  33. data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
  34. data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
  35. data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
  36. data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
  37. data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
  38. data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
  39. data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
  40. data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
  41. data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
  42. data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
  43. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
  44. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
  45. data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
  46. data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
  47. data/lib/langsmithrb_rails/client.rb +292 -0
  48. data/lib/langsmithrb_rails/config.rb +169 -0
  49. data/lib/langsmithrb_rails/evaluation/evaluator.rb +178 -0
  50. data/lib/langsmithrb_rails/evaluation/llm_evaluator.rb +154 -0
  51. data/lib/langsmithrb_rails/evaluation/string_evaluator.rb +158 -0
  52. data/lib/langsmithrb_rails/evaluation.rb +76 -0
  53. data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
  54. data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
  55. data/lib/langsmithrb_rails/langsmith.rb +35 -0
  56. data/lib/langsmithrb_rails/otel/exporter.rb +120 -0
  57. data/lib/langsmithrb_rails/otel.rb +135 -0
  58. data/lib/langsmithrb_rails/railtie.rb +33 -0
  59. data/lib/langsmithrb_rails/redactor.rb +76 -0
  60. data/lib/langsmithrb_rails/run_trees.rb +157 -0
  61. data/lib/langsmithrb_rails/version.rb +5 -0
  62. data/lib/langsmithrb_rails/wrappers/anthropic.rb +146 -0
  63. data/lib/langsmithrb_rails/wrappers/base.rb +81 -0
  64. data/lib/langsmithrb_rails/wrappers/llm.rb +151 -0
  65. data/lib/langsmithrb_rails/wrappers/openai.rb +193 -0
  66. data/lib/langsmithrb_rails/wrappers.rb +41 -0
  67. data/lib/langsmithrb_rails.rb +151 -0
  68. data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
  69. metadata +74 -7
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Generators
5
+ # Generator for adding LangSmith tracing to Rails applications
6
+ class TracingGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Adds LangSmith tracing to your Rails application"
10
+
11
+ def create_middleware
12
+ template "request_tracing.rb", "app/middleware/langsmithrb_rails/request_tracing.rb"
13
+ end
14
+
15
+ def create_service_concern
16
+ template "langsmith_traced.rb", "app/services/concerns/langsmith_traced.rb"
17
+ end
18
+
19
+ def create_job_concern
20
+ template "langsmith_traced_job.rb", "app/jobs/concerns/langsmith_traced_job.rb"
21
+ end
22
+
23
+ def update_application_config
24
+ application_rb_path = "config/application.rb"
25
+
26
+ if File.exist?(application_rb_path)
27
+ middleware_line = " config.middleware.use LangsmithrbRails::RequestTracing"
28
+
29
+ # Check if middleware is already configured
30
+ if File.read(application_rb_path).include?(middleware_line)
31
+ say_status :skip, "Middleware already configured in application.rb", :yellow
32
+ else
33
+ # Find the class Application < Rails::Application line
34
+ application_content = File.read(application_rb_path)
35
+
36
+ if application_content =~ /class Application < Rails::Application/
37
+ inject_into_file application_rb_path, after: "class Application < Rails::Application\n" do
38
+ <<~RUBY
39
+ # Use LangSmith request tracing middleware
40
+ #{middleware_line}
41
+
42
+ RUBY
43
+ end
44
+
45
+ say_status :insert, "Added middleware to application.rb", :green
46
+ else
47
+ say_status :error, "Could not find the Application class in application.rb", :red
48
+ end
49
+ end
50
+ else
51
+ say_status :error, "Could not find application.rb", :red
52
+ end
53
+ end
54
+
55
+ def create_directories
56
+ empty_directory "app/middleware/langsmithrb_rails"
57
+ empty_directory "app/services/concerns"
58
+ empty_directory "app/jobs/concerns"
59
+ end
60
+
61
+ def display_post_install_message
62
+ say "\n"
63
+ say "LangSmith tracing has been added to your Rails application! 🎉", :green
64
+ say "\n"
65
+ say "Usage:", :yellow
66
+ say " 1. Use the LangsmithTraced concern in your services:", :yellow
67
+ say " include LangsmithTraced", :yellow
68
+ say " LangsmithTraced.trace(name: 'my_operation', type: 'llm') { ... }", :yellow
69
+ say " 2. Use the LangsmithTracedJob concern in your jobs:", :yellow
70
+ say " include LangsmithTracedJob", :yellow
71
+ say "\n"
72
+ say "To add a local buffer for traces, run:", :yellow
73
+ say " bin/rails g langsmithrb_rails:buffer", :yellow
74
+ say "\n"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "securerandom"
7
+
8
+ module LangsmithrbRails
9
+ # Direct REST client for LangSmith API
10
+ class Client
11
+ # Initialize a new LangSmith client
12
+ # @param api_key [String] LangSmith API key
13
+ # @param api_url [String] LangSmith API URL
14
+ def initialize(api_key: Config[:api_key], api_url: Config[:api_url])
15
+ @api_key = api_key
16
+ @api_url = api_url.chomp("/")
17
+ end
18
+
19
+ # Create a new run in LangSmith
20
+ # @param payload [Hash] Run data
21
+ # @return [Hash] Response with status and body
22
+ def create_run(payload)
23
+ post("/runs", payload)
24
+ end
25
+
26
+ # Update an existing run in LangSmith
27
+ # @param id [String] Run ID
28
+ # @param payload [Hash] Updated run data
29
+ # @return [Hash] Response with status and body
30
+ def update_run(id, payload)
31
+ patch("/runs/#{id}", payload)
32
+ end
33
+
34
+ # Get a run by ID
35
+ # @param id [String] Run ID
36
+ # @return [Hash] Response with status and body
37
+ def get_run(id)
38
+ get("/runs/#{id}")
39
+ end
40
+
41
+ # List runs with optional filters
42
+ # @param project_name [String] Filter by project name
43
+ # @param trace_id [String] Filter by trace ID
44
+ # @param run_type [String] Filter by run type
45
+ # @param limit [Integer] Maximum number of runs to return
46
+ # @param offset [Integer] Offset for pagination
47
+ # @return [Hash] Response with status and body
48
+ def list_runs(project_name: nil, trace_id: nil, run_type: nil, limit: 100, offset: 0)
49
+ query_params = {
50
+ project_name: project_name,
51
+ trace_id: trace_id,
52
+ run_type: run_type,
53
+ limit: limit,
54
+ offset: offset
55
+ }.compact
56
+
57
+ query_string = query_params.map { |k, v| "#{k}=#{URI.encode_www_form_component(v.to_s)}" }.join("&")
58
+ get("/runs?#{query_string}")
59
+ end
60
+
61
+ # Create a dataset in LangSmith
62
+ # @param name [String] Dataset name
63
+ # @param description [String] Dataset description
64
+ # @return [Hash] Response with status and body
65
+ def create_dataset(name, description: nil)
66
+ payload = {
67
+ name: name,
68
+ description: description
69
+ }.compact
70
+ post("/datasets", payload)
71
+ end
72
+
73
+ # Get a dataset by ID
74
+ # @param id [String] Dataset ID
75
+ # @return [Hash] Response with status and body
76
+ def get_dataset(id)
77
+ get("/datasets/#{id}")
78
+ end
79
+
80
+ # List datasets
81
+ # @param limit [Integer] Maximum number of datasets to return
82
+ # @param offset [Integer] Offset for pagination
83
+ # @return [Hash] Response with status and body
84
+ def list_datasets(limit: 100, offset: 0)
85
+ get("/datasets?limit=#{limit}&offset=#{offset}")
86
+ end
87
+
88
+ # Create an example in a dataset
89
+ # @param dataset_id [String] Dataset ID
90
+ # @param inputs [Hash] Example inputs
91
+ # @param outputs [Hash] Example outputs (optional)
92
+ # @return [Hash] Response with status and body
93
+ def create_example(dataset_id, inputs, outputs = nil)
94
+ payload = {
95
+ dataset_id: dataset_id,
96
+ inputs: inputs,
97
+ outputs: outputs
98
+ }.compact
99
+ post("/examples", payload)
100
+ end
101
+
102
+ # Get an example by ID
103
+ # @param id [String] Example ID
104
+ # @return [Hash] Response with status and body
105
+ def get_example(id)
106
+ get("/examples/#{id}")
107
+ end
108
+
109
+ # List examples in a dataset
110
+ # @param dataset_id [String] Dataset ID
111
+ # @param limit [Integer] Maximum number of examples to return
112
+ # @param offset [Integer] Offset for pagination
113
+ # @return [Hash] Response with status and body
114
+ def list_examples(dataset_id, limit: 100, offset: 0)
115
+ get("/datasets/#{dataset_id}/examples?limit=#{limit}&offset=#{offset}")
116
+ end
117
+
118
+ # Create feedback for a run
119
+ # @param run_id [String] Run ID
120
+ # @param key [String] Feedback key
121
+ # @param score [Float, Integer, Boolean] Feedback score
122
+ # @param value [Hash] Additional feedback data (optional)
123
+ # @param comment [String] Feedback comment (optional)
124
+ # @return [Hash] Response with status and body
125
+ def create_feedback(run_id, key, score, value: nil, comment: nil)
126
+ payload = {
127
+ id: SecureRandom.uuid,
128
+ run_id: run_id,
129
+ key: key,
130
+ score: score,
131
+ value: value,
132
+ comment: comment
133
+ }.compact
134
+ post("/feedback", payload)
135
+ end
136
+
137
+ # Get feedback by ID
138
+ # @param id [String] Feedback ID
139
+ # @return [Hash] Response with status and body
140
+ def get_feedback(id)
141
+ get("/feedback/#{id}")
142
+ end
143
+
144
+ # List feedback for a run
145
+ # @param run_id [String] Run ID
146
+ # @return [Hash] Response with status and body
147
+ def list_feedback(run_id)
148
+ get("/runs/#{run_id}/feedback")
149
+ end
150
+
151
+ # Create a project in LangSmith
152
+ # @param name [String] Project name
153
+ # @param description [String] Project description (optional)
154
+ # @return [Hash] Response with status and body
155
+ def create_project(name, description: nil)
156
+ payload = {
157
+ name: name,
158
+ description: description
159
+ }.compact
160
+ post("/projects", payload)
161
+ end
162
+
163
+ # Get a project by name
164
+ # @param name [String] Project name
165
+ # @return [Hash] Response with status and body
166
+ def get_project(name)
167
+ get("/projects/#{URI.encode_www_form_component(name)}")
168
+ end
169
+
170
+ # List projects
171
+ # @param limit [Integer] Maximum number of projects to return
172
+ # @param offset [Integer] Offset for pagination
173
+ # @return [Hash] Response with status and body
174
+ def list_projects(limit: 100, offset: 0)
175
+ get("/projects?limit=#{limit}&offset=#{offset}")
176
+ end
177
+
178
+ # Upload a file attachment
179
+ # @param file_path [String] Path to the file
180
+ # @return [Hash] Response with status and body containing the file ID
181
+ def upload_file(file_path)
182
+ uri = URI.parse("#{@api_url}/files")
183
+
184
+ File.open(file_path, 'rb') do |file|
185
+ boundary = SecureRandom.hex(16)
186
+
187
+ req = Net::HTTP::Post.new(uri.request_uri)
188
+ req['Authorization'] = "Bearer #{@api_key}" if @api_key
189
+ req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
190
+
191
+ body = []
192
+ body << "--#{boundary}\r\n"
193
+ body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{File.basename(file_path)}\"\r\n"
194
+ body << "Content-Type: #{content_type_for_file(file_path)}\r\n\r\n"
195
+ body << file.read
196
+ body << "\r\n--#{boundary}--\r\n"
197
+
198
+ req.body = body.join
199
+
200
+ http = Net::HTTP.new(uri.host, uri.port)
201
+ http.use_ssl = uri.scheme == "https"
202
+ http.open_timeout = Config[:open_timeout_seconds]
203
+ http.read_timeout = Config[:timeout_seconds]
204
+
205
+ res = http.request(req)
206
+ {
207
+ status: res.code.to_i,
208
+ body: (JSON.parse(res.body) rescue { "raw" => res.body })
209
+ }
210
+ end
211
+ rescue => e
212
+ { status: 0, error: e.message }
213
+ end
214
+
215
+ private
216
+
217
+ # Make a GET request
218
+ # @param path [String] API path
219
+ # @return [Hash] Response with status and body
220
+ def get(path)
221
+ request(Net::HTTP::Get, path, nil)
222
+ end
223
+
224
+ # Make a POST request
225
+ # @param path [String] API path
226
+ # @param payload [Hash] Request payload
227
+ # @return [Hash] Response with status and body
228
+ def post(path, payload)
229
+ request(Net::HTTP::Post, path, payload)
230
+ end
231
+
232
+ # Make a PATCH request
233
+ # @param path [String] API path
234
+ # @param payload [Hash] Request payload
235
+ # @return [Hash] Response with status and body
236
+ def patch(path, payload)
237
+ request(Net::HTTP::Patch, path, payload)
238
+ end
239
+
240
+ # Make a DELETE request
241
+ # @param path [String] API path
242
+ # @return [Hash] Response with status and body
243
+ def delete(path)
244
+ request(Net::HTTP::Delete, path, nil)
245
+ end
246
+
247
+ # Make an HTTP request
248
+ # @param klass [Class] Net::HTTP request class
249
+ # @param path [String] API path
250
+ # @param payload [Hash] Request payload
251
+ # @return [Hash] Response with status and body
252
+ def request(klass, path, payload)
253
+ uri = URI.parse("#{@api_url}#{path}")
254
+ http = Net::HTTP.new(uri.host, uri.port)
255
+ http.use_ssl = uri.scheme == "https"
256
+ http.open_timeout = Config[:open_timeout_seconds]
257
+ http.read_timeout = Config[:timeout_seconds]
258
+
259
+ req = klass.new(uri.request_uri)
260
+ req["Authorization"] = "Bearer #{@api_key}" if @api_key
261
+
262
+ if payload
263
+ req["Content-Type"] = "application/json"
264
+ req.body = JSON.generate(payload)
265
+ end
266
+
267
+ res = http.request(req)
268
+ {
269
+ status: res.code.to_i,
270
+ body: (JSON.parse(res.body) rescue { "raw" => res.body })
271
+ }
272
+ rescue => e
273
+ { status: 0, error: e.message }
274
+ end
275
+
276
+ # Determine content type for a file
277
+ # @param file_path [String] Path to the file
278
+ # @return [String] Content type
279
+ def content_type_for_file(file_path)
280
+ ext = File.extname(file_path).downcase
281
+ case ext
282
+ when ".jpg", ".jpeg" then "image/jpeg"
283
+ when ".png" then "image/png"
284
+ when ".pdf" then "application/pdf"
285
+ when ".txt" then "text/plain"
286
+ when ".json" then "application/json"
287
+ when ".csv" then "text/csv"
288
+ else "application/octet-stream"
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "ostruct"
5
+ require "logger"
6
+ require "singleton"
7
+
8
+ module LangsmithrbRails
9
+ # Configuration class for LangsmithrbRails
10
+ class Config
11
+ include Singleton
12
+
13
+ # Default configuration values
14
+ DEFAULTS = {
15
+ api_url: "https://api.smith.langchain.com",
16
+ project_name: "default",
17
+ api_key: ENV["LANGSMITH_API_KEY"],
18
+ sampling: 1.0,
19
+ redact_by_default: true,
20
+ timeout_seconds: 3.0,
21
+ open_timeout_seconds: 1.0,
22
+ env: ENV["RAILS_ENV"] || "development",
23
+ enabled: true,
24
+ # Advanced tracing options
25
+ trace_all: ENV.fetch("LANGSMITH_TRACE_ALL", "false") == "true",
26
+ trace_level: ENV.fetch("LANGSMITH_TRACE_LEVEL", "info").to_sym,
27
+ # OpenTelemetry options
28
+ otel_enabled: ENV.fetch("LANGSMITH_OTEL_ENABLED", "false") == "true",
29
+ otel_service_name: ENV.fetch("LANGSMITH_OTEL_SERVICE_NAME", "langsmithrb_rails"),
30
+ # Evaluation options
31
+ evaluation_enabled: ENV.fetch("LANGSMITH_EVALUATION_ENABLED", "false") == "true",
32
+ # Logging options
33
+ log_level: ENV.fetch("LANGSMITH_LOG_LEVEL", "info").to_sym,
34
+ log_to_stdout: ENV.fetch("LANGSMITH_LOG_TO_STDOUT", "false") == "true"
35
+ }.freeze
36
+
37
+ # Configuration attributes
38
+ attr_accessor :api_key, :api_url, :project_name, :enabled, :sampling,
39
+ :trace_all, :trace_level, :otel_enabled, :otel_service_name,
40
+ :evaluation_enabled, :log_level, :log_to_stdout
41
+ attr_reader :logger
42
+
43
+ # Initialize with default values
44
+ def initialize
45
+ # Set defaults first
46
+ DEFAULTS.each do |key, value|
47
+ instance_variable_set("@#{key}", value)
48
+ end
49
+
50
+ # Then override with environment variables if present
51
+ load_from_env
52
+
53
+ # Initialize logger
54
+ @logger = create_logger(@log_level, @log_to_stdout)
55
+ end
56
+
57
+ # Load configuration from environment variables
58
+ # @return [Config] Self for chaining
59
+ def load_from_env
60
+ @api_key = ENV["LANGSMITH_API_KEY"] if ENV["LANGSMITH_API_KEY"]
61
+ @api_url = ENV["LANGSMITH_API_URL"] if ENV["LANGSMITH_API_URL"]
62
+ @project_name = ENV["LANGSMITH_PROJECT"] if ENV["LANGSMITH_PROJECT"]
63
+ @enabled = ENV["LANGSMITH_ENABLED"] == "true" if ENV.key?("LANGSMITH_ENABLED")
64
+ @sampling = ENV["LANGSMITH_SAMPLING"].to_f if ENV["LANGSMITH_SAMPLING"]
65
+ @trace_all = ENV["LANGSMITH_TRACE_ALL"] == "true" if ENV.key?("LANGSMITH_TRACE_ALL")
66
+ @trace_level = ENV["LANGSMITH_TRACE_LEVEL"].to_sym if ENV["LANGSMITH_TRACE_LEVEL"]
67
+ @otel_enabled = ENV["LANGSMITH_OTEL_ENABLED"] == "true" if ENV.key?("LANGSMITH_OTEL_ENABLED")
68
+ @otel_service_name = ENV["LANGSMITH_OTEL_SERVICE_NAME"] if ENV["LANGSMITH_OTEL_SERVICE_NAME"]
69
+ @evaluation_enabled = ENV["LANGSMITH_EVALUATION_ENABLED"] == "true" if ENV.key?("LANGSMITH_EVALUATION_ENABLED")
70
+ @log_level = ENV["LANGSMITH_LOG_LEVEL"].to_sym if ENV["LANGSMITH_LOG_LEVEL"]
71
+ @log_to_stdout = ENV["LANGSMITH_LOG_TO_STDOUT"] == "true" if ENV.key?("LANGSMITH_LOG_TO_STDOUT")
72
+ self
73
+ end
74
+
75
+ # Load configuration from YAML file
76
+ # @param path [String] Path to YAML file
77
+ # @return [Config] Self for chaining
78
+ def load_from_yaml(path)
79
+ return self unless File.exist?(path)
80
+
81
+ yml = YAML.load_file(path)
82
+ env = (ENV["RAILS_ENV"] || "development").to_s
83
+ config = yml.fetch(env, {})
84
+
85
+ config.each do |key, value|
86
+ send("#{key}=", value) if respond_to?("#{key}=")
87
+ end
88
+
89
+ # Reset logger if log settings changed
90
+ @logger = create_logger(@log_level, @log_to_stdout)
91
+
92
+ self
93
+ end
94
+
95
+ # Check if LangSmith is enabled
96
+ # @return [Boolean] Whether LangSmith is enabled
97
+ def enabled?
98
+ @enabled
99
+ end
100
+
101
+ # Check if a trace should be sampled
102
+ # @return [Boolean] Whether to sample the trace
103
+ def should_sample?
104
+ return true if @sampling >= 1.0
105
+ return false if @sampling <= 0.0
106
+ Random.rand < @sampling
107
+ end
108
+
109
+ # Check if OpenTelemetry is enabled
110
+ # @return [Boolean] Whether OpenTelemetry is enabled
111
+ def otel_enabled?
112
+ @otel_enabled
113
+ end
114
+
115
+ # Check if evaluation is enabled
116
+ # @return [Boolean] Whether evaluation is enabled
117
+ def evaluation_enabled?
118
+ @evaluation_enabled
119
+ end
120
+
121
+ # Set log level
122
+ # @param level [Symbol, String] Log level
123
+ # @return [Symbol] Log level
124
+ def log_level=(level)
125
+ level = level.to_sym if level.is_a?(String)
126
+ unless [:debug, :info, :warn, :error, :fatal].include?(level)
127
+ raise ArgumentError, "Invalid log level: #{level}"
128
+ end
129
+ @log_level = level
130
+ @logger = create_logger(@log_level, @log_to_stdout)
131
+ @log_level
132
+ end
133
+
134
+ # Get logger instance
135
+ # @return [Logger] Logger instance
136
+ def logger
137
+ @logger ||= create_logger(@log_level, @log_to_stdout)
138
+ end
139
+
140
+ private
141
+
142
+ # Create a logger
143
+ # @param level [Symbol] Log level
144
+ # @param to_stdout [Boolean] Whether to log to STDOUT
145
+ # @return [Logger] Logger instance
146
+ def create_logger(level, to_stdout)
147
+ logger = Logger.new(to_stdout ? STDOUT : IO::NULL)
148
+ logger.level = parse_log_level(level)
149
+ logger.formatter = proc do |severity, datetime, progname, msg|
150
+ "[LangsmithrbRails] [#{severity}] #{msg}\n"
151
+ end
152
+ logger
153
+ end
154
+
155
+ # Parse log level
156
+ # @param level [Symbol, String] Log level
157
+ # @return [Integer] Logger level constant
158
+ def parse_log_level(level)
159
+ case level.to_s.downcase
160
+ when "debug" then Logger::DEBUG
161
+ when "info" then Logger::INFO
162
+ when "warn" then Logger::WARN
163
+ when "error" then Logger::ERROR
164
+ when "fatal" then Logger::FATAL
165
+ else Logger::INFO
166
+ end
167
+ end
168
+ end
169
+ end