langsmithrb_rails 0.1.0 → 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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rspec_status +82 -0
  4. data/CHANGELOG.md +25 -0
  5. data/Gemfile +20 -0
  6. data/Gemfile.lock +321 -0
  7. data/LICENSE +21 -0
  8. data/README.md +268 -0
  9. data/Rakefile +10 -0
  10. data/langsmithrb_rails-0.1.0.gem +0 -0
  11. data/langsmithrb_rails.gemspec +45 -0
  12. data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
  13. data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
  14. data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
  15. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
  16. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
  17. data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
  18. data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
  19. data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
  20. data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
  21. data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
  22. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
  23. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
  24. data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
  25. data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
  26. data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
  27. data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
  28. data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
  29. data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
  30. data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
  31. data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
  32. data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
  33. data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
  34. data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
  35. data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
  36. data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
  37. data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
  38. data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
  39. data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
  40. data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
  41. data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
  42. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
  43. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
  44. data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
  45. data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
  46. data/lib/langsmithrb_rails/client.rb +77 -0
  47. data/lib/langsmithrb_rails/config.rb +72 -0
  48. data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
  49. data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
  50. data/lib/langsmithrb_rails/langsmith.rb +35 -0
  51. data/lib/langsmithrb_rails/railtie.rb +33 -0
  52. data/lib/langsmithrb_rails/redactor.rb +76 -0
  53. data/lib/langsmithrb_rails/version.rb +5 -0
  54. data/lib/langsmithrb_rails.rb +31 -0
  55. metadata +59 -6
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ namespace :langsmith do
8
+ desc "Run an evaluation on a dataset"
9
+ task :eval, [:dataset, :target, :experiment_name] => :environment do |_t, args|
10
+ dataset_name = args[:dataset] || "sample"
11
+ target_name = args[:target] || "http"
12
+ experiment_name = args[:experiment_name] || "eval_#{Time.now.strftime('%Y%m%d%H%M%S')}"
13
+
14
+ puts "Running evaluation with dataset '#{dataset_name}', target '#{target_name}', experiment '#{experiment_name}'"
15
+
16
+ # Load the dataset
17
+ dataset_path = Rails.root.join("config/langsmith/evals/datasets/#{dataset_name}.yml")
18
+ unless File.exist?(dataset_path)
19
+ puts "Error: Dataset not found at #{dataset_path}"
20
+ exit 1
21
+ end
22
+
23
+ dataset = YAML.load_file(dataset_path)
24
+ puts "Loaded dataset '#{dataset['name']}' with #{dataset['items'].size} items"
25
+
26
+ # Load the target
27
+ target_path = Rails.root.join("config/langsmith/evals/targets/#{target_name}.rb")
28
+ unless File.exist?(target_path)
29
+ puts "Error: Target not found at #{target_path}"
30
+ exit 1
31
+ end
32
+
33
+ # Load all check files
34
+ check_dir = Rails.root.join("config/langsmith/evals/checks")
35
+ Dir.glob(File.join(check_dir, "*.rb")).each do |file|
36
+ require file
37
+ end
38
+
39
+ # Initialize results
40
+ results = {
41
+ dataset: dataset["name"],
42
+ target: target_name,
43
+ experiment: experiment_name,
44
+ timestamp: Time.now.iso8601,
45
+ items: [],
46
+ summary: {
47
+ total: dataset["items"].size,
48
+ passed: 0,
49
+ failed: 0,
50
+ avg_score: 0.0
51
+ }
52
+ }
53
+
54
+ # Create target instance
55
+ target_class_name = target_name.capitalize
56
+ target_class = LangsmithrbRails::Evals::Targets.const_get(target_class_name)
57
+ target = target_class.new(ENV["LANGSMITH_EVAL_TARGET_CONFIG"] ? JSON.parse(ENV["LANGSMITH_EVAL_TARGET_CONFIG"]) : {})
58
+
59
+ # Run evaluations
60
+ dataset["items"].each_with_index do |item, index|
61
+ puts "Evaluating item #{index + 1}/#{dataset['items'].size}: #{item['id']}"
62
+
63
+ # Run the target
64
+ start_time = Time.now
65
+ response = target.run(item["input"])
66
+ end_time = Time.now
67
+
68
+ # Run checks
69
+ check_results = run_checks(item["input"], response, item["expected_output"])
70
+
71
+ # Determine if passed
72
+ passed = check_results.values.any? { |r| r[:passed] }
73
+
74
+ # Add to results
75
+ results[:items] << {
76
+ id: item["id"],
77
+ input: item["input"],
78
+ expected: item["expected_output"],
79
+ response: response,
80
+ checks: check_results,
81
+ passed: passed,
82
+ duration_ms: ((end_time - start_time) * 1000).to_i,
83
+ metadata: item["metadata"] || {}
84
+ }
85
+
86
+ # Update summary
87
+ results[:summary][:passed] += 1 if passed
88
+ results[:summary][:failed] += 1 unless passed
89
+ end
90
+
91
+ # Calculate average score
92
+ total_score = 0.0
93
+ total_checks = 0
94
+
95
+ results[:items].each do |item|
96
+ item[:checks].each do |_name, check|
97
+ total_score += check[:score]
98
+ total_checks += 1
99
+ end
100
+ end
101
+
102
+ results[:summary][:avg_score] = total_checks > 0 ? (total_score / total_checks) : 0.0
103
+
104
+ # Save results
105
+ output_dir = Rails.root.join("tmp/langsmith")
106
+ FileUtils.mkdir_p(output_dir)
107
+
108
+ output_path = output_dir.join("last_eval.json")
109
+ File.write(output_path, JSON.pretty_generate(results))
110
+
111
+ # Print summary
112
+ puts "\nEvaluation complete!"
113
+ puts "Dataset: #{dataset['name']}"
114
+ puts "Target: #{target_name}"
115
+ puts "Experiment: #{experiment_name}"
116
+ puts "Results: #{results[:summary][:passed]}/#{results[:summary][:total]} passed (#{(results[:summary][:avg_score] * 100).round(1)}%)"
117
+ puts "Results saved to #{output_path}"
118
+
119
+ # Exit with error if below threshold
120
+ threshold = ENV["LANGSMITH_EVAL_THRESHOLD"] ? ENV["LANGSMITH_EVAL_THRESHOLD"].to_f : 0.7
121
+ if results[:summary][:avg_score] < threshold
122
+ puts "\nEvaluation failed: score #{(results[:summary][:avg_score] * 100).round(1)}% is below threshold #{(threshold * 100).round(1)}%"
123
+ exit 1
124
+ end
125
+ end
126
+
127
+ desc "Compare two experiments"
128
+ task :compare, [:exp_a, :exp_b] => :environment do |_t, args|
129
+ exp_a = args[:exp_a]
130
+ exp_b = args[:exp_b]
131
+
132
+ unless exp_a && exp_b
133
+ puts "Error: Both experiment names are required"
134
+ puts "Usage: rake langsmith:compare[exp_a,exp_b]"
135
+ exit 1
136
+ end
137
+
138
+ # Load experiment results
139
+ output_dir = Rails.root.join("tmp/langsmith")
140
+
141
+ exp_a_path = output_dir.join("#{exp_a}.json")
142
+ exp_b_path = output_dir.join("#{exp_b}.json")
143
+
144
+ unless File.exist?(exp_a_path) && File.exist?(exp_b_path)
145
+ puts "Error: Experiment results not found"
146
+ exit 1
147
+ end
148
+
149
+ exp_a_results = JSON.parse(File.read(exp_a_path))
150
+ exp_b_results = JSON.parse(File.read(exp_b_path))
151
+
152
+ # Compare results
153
+ comparison = {
154
+ exp_a: {
155
+ name: exp_a,
156
+ passed: exp_a_results["summary"]["passed"],
157
+ total: exp_a_results["summary"]["total"],
158
+ avg_score: exp_a_results["summary"]["avg_score"]
159
+ },
160
+ exp_b: {
161
+ name: exp_b,
162
+ passed: exp_b_results["summary"]["passed"],
163
+ total: exp_b_results["summary"]["total"],
164
+ avg_score: exp_b_results["summary"]["avg_score"]
165
+ },
166
+ diff: {
167
+ passed: exp_b_results["summary"]["passed"] - exp_a_results["summary"]["passed"],
168
+ avg_score: exp_b_results["summary"]["avg_score"] - exp_a_results["summary"]["avg_score"]
169
+ },
170
+ items: []
171
+ }
172
+
173
+ # Compare individual items
174
+ exp_a_items = exp_a_results["items"].index_by { |item| item["id"] }
175
+ exp_b_items = exp_b_results["items"].index_by { |item| item["id"] }
176
+
177
+ all_item_ids = (exp_a_items.keys + exp_b_items.keys).uniq
178
+
179
+ all_item_ids.each do |id|
180
+ item_a = exp_a_items[id]
181
+ item_b = exp_b_items[id]
182
+
183
+ next unless item_a && item_b
184
+
185
+ # Calculate score difference
186
+ score_a = calculate_avg_score(item_a["checks"])
187
+ score_b = calculate_avg_score(item_b["checks"])
188
+
189
+ comparison[:items] << {
190
+ id: id,
191
+ exp_a: {
192
+ passed: item_a["passed"],
193
+ score: score_a
194
+ },
195
+ exp_b: {
196
+ passed: item_b["passed"],
197
+ score: score_b
198
+ },
199
+ diff: {
200
+ passed: item_b["passed"] != item_a["passed"],
201
+ score: score_b - score_a
202
+ }
203
+ }
204
+ end
205
+
206
+ # Sort by score difference
207
+ comparison[:items].sort_by! { |item| -item[:diff][:score].abs }
208
+
209
+ # Save comparison
210
+ comparison_path = output_dir.join("comparison_#{exp_a}_#{exp_b}.json")
211
+ File.write(comparison_path, JSON.pretty_generate(comparison))
212
+
213
+ # Print summary
214
+ puts "\nComparison complete!"
215
+ puts "Experiment A: #{exp_a} - #{comparison[:exp_a][:passed]}/#{comparison[:exp_a][:total]} passed (#{(comparison[:exp_a][:avg_score] * 100).round(1)}%)"
216
+ puts "Experiment B: #{exp_b} - #{comparison[:exp_b][:passed]}/#{comparison[:exp_b][:total]} passed (#{(comparison[:exp_b][:avg_score] * 100).round(1)}%)"
217
+ puts "Difference: #{comparison[:diff][:passed] >= 0 ? '+' : ''}#{comparison[:diff][:passed]} passed, #{comparison[:diff][:avg_score] >= 0 ? '+' : ''}#{(comparison[:diff][:avg_score] * 100).round(1)}%"
218
+ puts "Results saved to #{comparison_path}"
219
+
220
+ # Print top differences
221
+ puts "\nTop differences:"
222
+ comparison[:items].first(5).each do |item|
223
+ puts "#{item[:id]}: #{(item[:diff][:score] * 100).round(1)}% #{item[:diff][:score] >= 0 ? 'improvement' : 'regression'}"
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ # Run all available checks on a response
230
+ def run_checks(input, response, expected)
231
+ results = {}
232
+
233
+ # Find all check classes
234
+ LangsmithrbRails::Evals::Checks.constants.each do |const|
235
+ check_class = LangsmithrbRails::Evals::Checks.const_get(const)
236
+ next unless check_class.respond_to?(:evaluate)
237
+
238
+ # Run the check
239
+ check_name = const.to_s.underscore
240
+ results[check_name] = check_class.evaluate(input, response, expected)
241
+ end
242
+
243
+ results
244
+ end
245
+
246
+ # Calculate average score for an item
247
+ def calculate_avg_score(checks)
248
+ total_score = 0.0
249
+ checks.each do |_name, check|
250
+ total_score += check["score"]
251
+ end
252
+
253
+ checks.empty? ? 0.0 : (total_score / checks.size)
254
+ end
255
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module LangsmithrbRails
8
+ module Evals
9
+ module Targets
10
+ # HTTP target for evaluating LLM responses via API calls
11
+ class Http
12
+ # Initialize the target
13
+ # @param config [Hash] Configuration options
14
+ def initialize(config = {})
15
+ @url = config["url"] || ENV["LANGSMITH_EVAL_TARGET_URL"]
16
+ @headers = config["headers"] || {}
17
+ @method = (config["method"] || "post").downcase
18
+ @timeout = config["timeout"] || 30
19
+
20
+ # Add default headers
21
+ @headers["Content-Type"] ||= "application/json"
22
+ @headers["Accept"] ||= "application/json"
23
+
24
+ # Add authorization if provided
25
+ if config["api_key"]
26
+ @headers["Authorization"] ||= "Bearer #{config["api_key"]}"
27
+ end
28
+ end
29
+
30
+ # Run the target with the given input
31
+ # @param input [Hash] Input data
32
+ # @return [Hash] Response data
33
+ def run(input)
34
+ # Validate URL
35
+ unless @url
36
+ return { error: "No target URL specified" }
37
+ end
38
+
39
+ # Create URI
40
+ uri = URI.parse(@url)
41
+
42
+ # Create HTTP client
43
+ http = Net::HTTP.new(uri.host, uri.port)
44
+ http.use_ssl = uri.scheme == "https"
45
+ http.open_timeout = @timeout
46
+ http.read_timeout = @timeout
47
+
48
+ # Create request
49
+ request = create_request(uri, input)
50
+
51
+ # Add headers
52
+ @headers.each do |key, value|
53
+ request[key] = value
54
+ end
55
+
56
+ # Send request
57
+ response = http.request(request)
58
+
59
+ # Parse response
60
+ parse_response(response)
61
+ rescue => e
62
+ { error: e.message }
63
+ end
64
+
65
+ private
66
+
67
+ # Create the HTTP request
68
+ # @param uri [URI] Request URI
69
+ # @param input [Hash] Input data
70
+ # @return [Net::HTTPRequest] HTTP request
71
+ def create_request(uri, input)
72
+ case @method
73
+ when "get"
74
+ # For GET requests, add parameters to the URL
75
+ query = URI.encode_www_form(input)
76
+ uri.query = query
77
+ Net::HTTP::Get.new(uri)
78
+ when "post"
79
+ # For POST requests, add parameters to the body
80
+ request = Net::HTTP::Post.new(uri)
81
+ request.body = JSON.generate(input)
82
+ request
83
+ when "put"
84
+ # For PUT requests, add parameters to the body
85
+ request = Net::HTTP::Put.new(uri)
86
+ request.body = JSON.generate(input)
87
+ request
88
+ else
89
+ # Default to POST
90
+ request = Net::HTTP::Post.new(uri)
91
+ request.body = JSON.generate(input)
92
+ request
93
+ end
94
+ end
95
+
96
+ # Parse the HTTP response
97
+ # @param response [Net::HTTPResponse] HTTP response
98
+ # @return [Hash] Parsed response
99
+ def parse_response(response)
100
+ if response.code.to_i >= 200 && response.code.to_i < 300
101
+ # Successful response
102
+ if response["Content-Type"]&.include?("application/json")
103
+ # Parse JSON response
104
+ JSON.parse(response.body)
105
+ else
106
+ # Return raw response
107
+ { text: response.body }
108
+ end
109
+ else
110
+ # Error response
111
+ {
112
+ error: "HTTP error #{response.code}",
113
+ body: response.body
114
+ }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Evals
5
+ module Targets
6
+ # Ruby target for evaluating LLM responses via direct Ruby calls
7
+ class Ruby
8
+ # Initialize the target
9
+ # @param config [Hash] Configuration options
10
+ def initialize(config = {})
11
+ @class_name = config["class"]
12
+ @method_name = config["method"]
13
+ @args = config["args"] || []
14
+ @kwargs = config["kwargs"] || {}
15
+ end
16
+
17
+ # Run the target with the given input
18
+ # @param input [Hash] Input data
19
+ # @return [Hash] Response data
20
+ def run(input)
21
+ # Validate configuration
22
+ unless @class_name && @method_name
23
+ return { error: "Class and method must be specified" }
24
+ end
25
+
26
+ # Find the class
27
+ klass = find_class(@class_name)
28
+ unless klass
29
+ return { error: "Class #{@class_name} not found" }
30
+ end
31
+
32
+ # Check if the method exists
33
+ unless klass.respond_to?(@method_name) || klass.instance_methods.include?(@method_name.to_sym)
34
+ return { error: "Method #{@method_name} not found in #{@class_name}" }
35
+ end
36
+
37
+ # Prepare arguments
38
+ args = prepare_args(input)
39
+ kwargs = prepare_kwargs(input)
40
+
41
+ # Call the method
42
+ result = call_method(klass, args, kwargs)
43
+
44
+ # Format the result
45
+ format_result(result)
46
+ rescue => e
47
+ { error: e.message, backtrace: e.backtrace&.first(5) }
48
+ end
49
+
50
+ private
51
+
52
+ # Find the class by name
53
+ # @param class_name [String] Class name
54
+ # @return [Class, nil] Class or nil if not found
55
+ def find_class(class_name)
56
+ class_name.split("::").inject(Object) do |mod, class_name|
57
+ mod.const_get(class_name)
58
+ end
59
+ rescue NameError
60
+ nil
61
+ end
62
+
63
+ # Prepare arguments for the method call
64
+ # @param input [Hash] Input data
65
+ # @return [Array] Arguments for the method call
66
+ def prepare_args(input)
67
+ @args.map do |arg|
68
+ if arg.is_a?(String) && arg.start_with?("$")
69
+ # Replace placeholders with input values
70
+ key = arg[1..-1]
71
+ input[key] || input[key.to_sym]
72
+ else
73
+ arg
74
+ end
75
+ end
76
+ end
77
+
78
+ # Prepare keyword arguments for the method call
79
+ # @param input [Hash] Input data
80
+ # @return [Hash] Keyword arguments for the method call
81
+ def prepare_kwargs(input)
82
+ result = {}
83
+
84
+ @kwargs.each do |key, value|
85
+ if value.is_a?(String) && value.start_with?("$")
86
+ # Replace placeholders with input values
87
+ input_key = value[1..-1]
88
+ result[key.to_sym] = input[input_key] || input[input_key.to_sym]
89
+ else
90
+ result[key.to_sym] = value
91
+ end
92
+ end
93
+
94
+ # Add all input values as keyword arguments if not already present
95
+ input.each do |key, value|
96
+ key_sym = key.to_sym
97
+ result[key_sym] = value unless result.key?(key_sym)
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ # Call the method
104
+ # @param klass [Class] Class to call the method on
105
+ # @param args [Array] Arguments for the method call
106
+ # @param kwargs [Hash] Keyword arguments for the method call
107
+ # @return [Object] Result of the method call
108
+ def call_method(klass, args, kwargs)
109
+ if klass.respond_to?(@method_name)
110
+ # Call class method
111
+ klass.send(@method_name, *args, **kwargs)
112
+ else
113
+ # Call instance method
114
+ klass.new.send(@method_name, *args, **kwargs)
115
+ end
116
+ end
117
+
118
+ # Format the result
119
+ # @param result [Object] Result of the method call
120
+ # @return [Hash] Formatted result
121
+ def format_result(result)
122
+ case result
123
+ when Hash
124
+ result
125
+ when String
126
+ { text: result }
127
+ when Array
128
+ { items: result }
129
+ else
130
+ { output: result }
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Generators
5
+ # Generator for installing LangSmith Rails integration
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Creates LangSmith configuration files for your Rails application"
10
+
11
+ def create_initializer
12
+ template "initializer.rb", "config/initializers/langsmith.rb"
13
+ end
14
+
15
+ def create_config_file
16
+ template "config.yml", "config/langsmith.yml"
17
+ end
18
+
19
+ def display_post_install_message
20
+ say "\n"
21
+ say "LangSmith Rails has been installed! 🎉", :green
22
+ say "\n"
23
+ say "Next steps:", :yellow
24
+ say " 1. Add your LangSmith API key to your environment:", :yellow
25
+ say " export LANGSMITH_API_KEY=your_api_key", :yellow
26
+ say " 2. Optionally set your LangSmith project name:", :yellow
27
+ say " export LANGSMITH_PROJECT=your_project_name", :yellow
28
+ say "\n"
29
+ say "To enable tracing in your Rails app, run:", :yellow
30
+ say " bin/rails g langsmithrb_rails:tracing", :yellow
31
+ say "\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ # LangSmith Rails Configuration
2
+ # This file was generated by the langsmithrb_rails:install generator.
3
+ # You can customize this file to configure LangSmith for your Rails application.
4
+
5
+ # Default configuration for all environments
6
+ default: &default
7
+ # LangSmith API URL
8
+ api_url: https://api.smith.langchain.com
9
+
10
+ # Whether to redact PII by default
11
+ redact_by_default: true
12
+
13
+ # HTTP request timeouts (in seconds)
14
+ timeout_seconds: 3.0
15
+ open_timeout_seconds: 1.0
16
+
17
+ # Development environment configuration
18
+ development:
19
+ <<: *default
20
+ # Enable tracing in development
21
+ enabled: true
22
+ # Sample 100% of requests in development
23
+ sampling_rate: 1.0
24
+ # Disable PII redaction in development
25
+ redact_by_default: false
26
+
27
+ # Test environment configuration
28
+ test:
29
+ <<: *default
30
+ # Enable tracing in test
31
+ enabled: true
32
+ # Sample 100% of requests in test
33
+ sampling_rate: 1.0
34
+ # Disable PII redaction in test
35
+ redact_by_default: false
36
+
37
+ # Production environment configuration
38
+ production:
39
+ <<: *default
40
+ # Enable tracing in production if API key is present
41
+ enabled: <%= ENV["LANGSMITH_API_KEY"].present? %>
42
+ # Sample 10% of requests in production
43
+ sampling_rate: 0.1
44
+ # Enable PII redaction in production
45
+ redact_by_default: true
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # LangSmith Rails Initializer
4
+ # This file was generated by the langsmithrb_rails:install generator.
5
+ # You can customize this file to configure LangSmith for your Rails application.
6
+
7
+ # Configure LangSmith Rails
8
+ LangsmithrbRails.configure do |config|
9
+ # Whether LangSmith tracing is enabled
10
+ # Defaults to true if LANGSMITH_API_KEY is present
11
+ config.enabled = ENV["LANGSMITH_API_KEY"].present?
12
+
13
+ # LangSmith API key
14
+ # Set this to your LangSmith API key
15
+ config.api_key = ENV["LANGSMITH_API_KEY"]
16
+
17
+ # LangSmith project name
18
+ # Set this to your LangSmith project name
19
+ config.project_name = ENV["LANGSMITH_PROJECT"]
20
+ end
21
+
22
+ # Configure LangSmith client directly if enabled
23
+ if LangsmithrbRails.config.enabled && LangsmithrbRails.config.api_key
24
+ Langsmithrb.configure do |config|
25
+ config.api_key = LangsmithrbRails.config.api_key
26
+ config.project_name = LangsmithrbRails.config.project_name
27
+ config.tracing_enabled = true
28
+ end
29
+ end
30
+
31
+ # Load environment-specific configuration from config/langsmith.yml
32
+ Rails.application.config.after_initialize do
33
+ LangsmithrbRails::Config.load!(rails_root: Rails.root)
34
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Generators
5
+ # Generator for adding privacy features to Rails applications
6
+ class PrivacyGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ desc "Adds privacy features for LangSmith traces"
10
+
11
+ def create_custom_redactor
12
+ template "custom_redactor.rb", "app/lib/langsmithrb_rails/custom_redactor.rb"
13
+ end
14
+
15
+ def create_initializer
16
+ template "privacy_initializer.rb", "config/initializers/langsmith_privacy.rb"
17
+ end
18
+
19
+ def create_config_file
20
+ template "privacy.yml", "config/langsmith_privacy.yml"
21
+ end
22
+
23
+ def display_post_install_message
24
+ say "\n"
25
+ say "LangSmith privacy features have been added to your Rails application! 🎉", :green
26
+ say "\n"
27
+ say "This adds:", :yellow
28
+ say " 1. Custom redactor for PII in app/lib/langsmithrb_rails/custom_redactor.rb", :yellow
29
+ say " 2. Privacy initializer in config/initializers/langsmith_privacy.rb", :yellow
30
+ say " 3. Privacy configuration in config/langsmith_privacy.yml", :yellow
31
+ say "\n"
32
+ say "To customize redaction:", :yellow
33
+ say " 1. Edit config/langsmith_privacy.yml to configure allowlists and patterns", :yellow
34
+ say " 2. Modify app/lib/langsmithrb_rails/custom_redactor.rb to add custom patterns", :yellow
35
+ say "\n"
36
+ end
37
+ end
38
+ end
39
+ end