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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rspec_status +82 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +321 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/Rakefile +10 -0
- data/langsmithrb_rails-0.1.0.gem +0 -0
- data/langsmithrb_rails.gemspec +45 -0
- data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
- data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
- data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
- data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
- data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
- data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
- data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
- data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
- data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
- data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
- data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
- data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
- data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
- data/lib/langsmithrb_rails/client.rb +77 -0
- data/lib/langsmithrb_rails/config.rb +72 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
- data/lib/langsmithrb_rails/langsmith.rb +35 -0
- data/lib/langsmithrb_rails/railtie.rb +33 -0
- data/lib/langsmithrb_rails/redactor.rb +76 -0
- data/lib/langsmithrb_rails/version.rb +5 -0
- data/lib/langsmithrb_rails.rb +31 -0
- 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
|