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,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LangsmithrbRails
|
4
|
+
# Custom redactor for PII in LangSmith traces
|
5
|
+
class CustomRedactor
|
6
|
+
# Initialize the redactor with configuration
|
7
|
+
# @param config [Hash] Configuration options
|
8
|
+
def initialize(config = {})
|
9
|
+
@config = config || {}
|
10
|
+
@base_redactor = LangsmithrbRails::Redactor.new(config)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Redact PII from text
|
14
|
+
# @param text [String] Text to redact
|
15
|
+
# @return [String] Redacted text
|
16
|
+
def redact(text)
|
17
|
+
return text unless text.is_a?(String)
|
18
|
+
|
19
|
+
# First apply the base redactor
|
20
|
+
result = @base_redactor.redact(text)
|
21
|
+
|
22
|
+
# Then apply custom patterns
|
23
|
+
custom_patterns.each do |pattern_name, pattern|
|
24
|
+
result = apply_pattern(result, pattern_name, pattern)
|
25
|
+
end
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
# Redact PII from a hash or array
|
31
|
+
# @param data [Hash, Array] Data to redact
|
32
|
+
# @return [Hash, Array] Redacted data
|
33
|
+
def redact_data(data)
|
34
|
+
case data
|
35
|
+
when String
|
36
|
+
redact(data)
|
37
|
+
when Hash
|
38
|
+
redact_hash(data)
|
39
|
+
when Array
|
40
|
+
redact_array(data)
|
41
|
+
else
|
42
|
+
data
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Redact PII from a hash
|
49
|
+
# @param hash [Hash] Hash to redact
|
50
|
+
# @return [Hash] Redacted hash
|
51
|
+
def redact_hash(hash)
|
52
|
+
result = {}
|
53
|
+
|
54
|
+
hash.each do |key, value|
|
55
|
+
# Skip redaction for allowlisted keys
|
56
|
+
if allowlisted_key?(key.to_s)
|
57
|
+
result[key] = value
|
58
|
+
else
|
59
|
+
result[key] = redact_data(value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
# Redact PII from an array
|
67
|
+
# @param array [Array] Array to redact
|
68
|
+
# @return [Array] Redacted array
|
69
|
+
def redact_array(array)
|
70
|
+
array.map { |item| redact_data(item) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check if a key is allowlisted
|
74
|
+
# @param key [String] Key to check
|
75
|
+
# @return [Boolean] Whether the key is allowlisted
|
76
|
+
def allowlisted_key?(key)
|
77
|
+
(@config[:allowlist_keys] || []).any? do |pattern|
|
78
|
+
if pattern.is_a?(Regexp)
|
79
|
+
key =~ pattern
|
80
|
+
else
|
81
|
+
key == pattern.to_s
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Apply a redaction pattern
|
87
|
+
# @param text [String] Text to redact
|
88
|
+
# @param pattern_name [Symbol] Pattern name
|
89
|
+
# @param pattern [Regexp, Hash] Pattern to apply
|
90
|
+
# @return [String] Redacted text
|
91
|
+
def apply_pattern(text, pattern_name, pattern)
|
92
|
+
# Skip if the pattern is disabled
|
93
|
+
return text if @config[:disabled_patterns]&.include?(pattern_name.to_s)
|
94
|
+
|
95
|
+
if pattern.is_a?(Regexp)
|
96
|
+
# Simple regex replacement
|
97
|
+
text.gsub(pattern, "[REDACTED:#{pattern_name.upcase}]")
|
98
|
+
elsif pattern.is_a?(Hash) && pattern[:pattern].is_a?(Regexp)
|
99
|
+
# Advanced pattern with custom replacement
|
100
|
+
replacement = pattern[:replacement] || "[REDACTED:#{pattern_name.upcase}]"
|
101
|
+
text.gsub(pattern[:pattern], replacement)
|
102
|
+
else
|
103
|
+
# Invalid pattern
|
104
|
+
text
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Custom redaction patterns
|
109
|
+
# @return [Hash] Custom patterns
|
110
|
+
def custom_patterns
|
111
|
+
{
|
112
|
+
# Example: Redact API keys
|
113
|
+
api_key: /\b(?:api[_-]?key|access[_-]?token)[=:]\s*["']?([a-zA-Z0-9]{20,})/i,
|
114
|
+
|
115
|
+
# Example: Redact database connection strings
|
116
|
+
db_connection: {
|
117
|
+
pattern: /(?:postgres|mysql|mongodb):\/\/[^\s"']+/i,
|
118
|
+
replacement: "[REDACTED:DATABASE_URL]"
|
119
|
+
},
|
120
|
+
|
121
|
+
# Example: Redact JWT tokens
|
122
|
+
jwt: /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/,
|
123
|
+
|
124
|
+
# Example: Redact session IDs
|
125
|
+
session_id: /\b(?:session_id|sid)=([a-zA-Z0-9]{16,})/i,
|
126
|
+
|
127
|
+
# Add your own custom patterns here
|
128
|
+
# custom_pattern: /pattern/
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# LangSmith Privacy Configuration
|
2
|
+
# This file configures PII redaction for LangSmith traces
|
3
|
+
|
4
|
+
# Default configuration for all environments
|
5
|
+
default: &default
|
6
|
+
# Enable or disable PII redaction
|
7
|
+
redact_pii: true
|
8
|
+
|
9
|
+
# Fields that should never be redacted
|
10
|
+
allowlist_keys:
|
11
|
+
- id
|
12
|
+
- created_at
|
13
|
+
- updated_at
|
14
|
+
- type
|
15
|
+
- status
|
16
|
+
- count
|
17
|
+
- total
|
18
|
+
- name
|
19
|
+
- title
|
20
|
+
- description
|
21
|
+
- category
|
22
|
+
- tags
|
23
|
+
- "*.id"
|
24
|
+
- "*.type"
|
25
|
+
- "*.status"
|
26
|
+
|
27
|
+
# Patterns that should be disabled
|
28
|
+
disabled_patterns: []
|
29
|
+
|
30
|
+
# Specific fields to always redact
|
31
|
+
redact_fields:
|
32
|
+
- password
|
33
|
+
- token
|
34
|
+
- secret
|
35
|
+
- key
|
36
|
+
- credential
|
37
|
+
- auth
|
38
|
+
- ssn
|
39
|
+
- social_security
|
40
|
+
- credit_card
|
41
|
+
- cc_number
|
42
|
+
- cvv
|
43
|
+
- user_data
|
44
|
+
- personal_info
|
45
|
+
|
46
|
+
# Development environment
|
47
|
+
development:
|
48
|
+
<<: *default
|
49
|
+
# You can override settings for development here
|
50
|
+
# For example, to disable redaction in development:
|
51
|
+
# redact_pii: false
|
52
|
+
|
53
|
+
# Test environment
|
54
|
+
test:
|
55
|
+
<<: *default
|
56
|
+
# Test-specific settings
|
57
|
+
redact_pii: true
|
58
|
+
|
59
|
+
# Production environment
|
60
|
+
production:
|
61
|
+
<<: *default
|
62
|
+
# Production-specific settings
|
63
|
+
redact_pii: true
|
64
|
+
|
65
|
+
# Additional fields to redact in production
|
66
|
+
redact_fields:
|
67
|
+
- password
|
68
|
+
- token
|
69
|
+
- secret
|
70
|
+
- key
|
71
|
+
- credential
|
72
|
+
- auth
|
73
|
+
- ssn
|
74
|
+
- social_security
|
75
|
+
- credit_card
|
76
|
+
- cc_number
|
77
|
+
- cvv
|
78
|
+
- user_data
|
79
|
+
- personal_info
|
80
|
+
- email
|
81
|
+
- phone
|
82
|
+
- address
|
83
|
+
- zip
|
84
|
+
- postal
|
85
|
+
- birthdate
|
86
|
+
- dob
|
87
|
+
- ip_address
|
88
|
+
- location
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Load custom redactor
|
4
|
+
require_relative "../../app/lib/langsmithrb_rails/custom_redactor"
|
5
|
+
|
6
|
+
# Load privacy configuration
|
7
|
+
privacy_config_path = Rails.root.join("config/langsmith_privacy.yml")
|
8
|
+
privacy_config = if File.exist?(privacy_config_path)
|
9
|
+
YAML.load_file(privacy_config_path)[Rails.env] || {}
|
10
|
+
else
|
11
|
+
{}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convert string keys to symbols
|
15
|
+
privacy_config = privacy_config.transform_keys(&:to_sym)
|
16
|
+
|
17
|
+
# Convert allowlist patterns to regex if they use wildcard syntax
|
18
|
+
if privacy_config[:allowlist_keys].is_a?(Array)
|
19
|
+
privacy_config[:allowlist_keys] = privacy_config[:allowlist_keys].map do |pattern|
|
20
|
+
if pattern.is_a?(String) && pattern.include?("*")
|
21
|
+
# Convert glob pattern to regex
|
22
|
+
Regexp.new("^#{Regexp.escape(pattern).gsub("\\*", ".*")}$")
|
23
|
+
else
|
24
|
+
pattern
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initialize the custom redactor
|
30
|
+
LangsmithrbRails.configure do |config|
|
31
|
+
# Use the custom redactor for PII redaction
|
32
|
+
config.redactor = LangsmithrbRails::CustomRedactor.new(privacy_config)
|
33
|
+
|
34
|
+
# Configure privacy-related settings
|
35
|
+
config.redact_pii = privacy_config[:redact_pii] != false
|
36
|
+
|
37
|
+
# Optionally configure specific fields to redact
|
38
|
+
if privacy_config[:redact_fields].is_a?(Array)
|
39
|
+
config.redact_fields = privacy_config[:redact_fields]
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Concern for adding LangSmith tracing to services
|
4
|
+
module LangsmithTraced
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Trace a block of code with LangSmith
|
9
|
+
# @param name [String] Name of the trace
|
10
|
+
# @param type [String] Type of the trace (llm, tool, retriever, etc.)
|
11
|
+
# @param meta [Hash] Additional metadata for the trace
|
12
|
+
# @return [Object] Result of the block
|
13
|
+
def trace(name:, type: "llm", meta: {})
|
14
|
+
# Skip tracing if sampling rate check fails
|
15
|
+
return yield if rand > LangsmithrbRails::Config[:sampling_rate]
|
16
|
+
|
17
|
+
# Get the start time
|
18
|
+
started = Time.now
|
19
|
+
|
20
|
+
# Extract and redact input if present
|
21
|
+
input = meta.delete(:input)
|
22
|
+
input = LangsmithrbRails::Redactor.scrub(input) if input
|
23
|
+
|
24
|
+
# Create a client
|
25
|
+
client = LangsmithrbRails::Client.new
|
26
|
+
|
27
|
+
# Prepare the run payload
|
28
|
+
run_payload = {
|
29
|
+
name: name,
|
30
|
+
run_type: type,
|
31
|
+
meta: meta.merge(started_at: started.iso8601),
|
32
|
+
parent_run_id: Thread.current[:langsmith_run_id]
|
33
|
+
}
|
34
|
+
run_payload[:input] = input if input
|
35
|
+
|
36
|
+
# Create the run
|
37
|
+
create_response = send_trace("create", nil, run_payload)
|
38
|
+
run_id = create_response&.dig(:body, "id")
|
39
|
+
|
40
|
+
# Store the previous run ID
|
41
|
+
previous_run_id = Thread.current[:langsmith_run_id]
|
42
|
+
|
43
|
+
# Set the current run ID for nested traces
|
44
|
+
Thread.current[:langsmith_run_id] = run_id if run_id
|
45
|
+
|
46
|
+
# Execute the block
|
47
|
+
result = yield
|
48
|
+
|
49
|
+
# Restore the previous run ID
|
50
|
+
Thread.current[:langsmith_run_id] = previous_run_id
|
51
|
+
|
52
|
+
# Update the run with the result
|
53
|
+
if run_id
|
54
|
+
update_payload = {
|
55
|
+
ended_at: Time.now.iso8601
|
56
|
+
}
|
57
|
+
|
58
|
+
# Add output if present
|
59
|
+
if result
|
60
|
+
update_payload[:output] = LangsmithrbRails::Redactor.scrub(result)
|
61
|
+
end
|
62
|
+
|
63
|
+
send_trace("update", run_id, update_payload)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the result
|
67
|
+
result
|
68
|
+
rescue => e
|
69
|
+
# Restore the previous run ID
|
70
|
+
Thread.current[:langsmith_run_id] = previous_run_id
|
71
|
+
|
72
|
+
# Update the run with the error
|
73
|
+
if run_id
|
74
|
+
begin
|
75
|
+
send_trace("update", run_id, {
|
76
|
+
ended_at: Time.now.iso8601,
|
77
|
+
error: e.message
|
78
|
+
})
|
79
|
+
rescue => trace_error
|
80
|
+
Rails.logger.error("LangSmith error update failed: #{trace_error.message}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Re-raise the original error
|
85
|
+
raise
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Send a trace to LangSmith
|
91
|
+
# @param action [String] Action to perform (create or update)
|
92
|
+
# @param run_id [String, nil] Run ID for updates
|
93
|
+
# @param payload [Hash] Payload to send
|
94
|
+
# @return [Hash, nil] Response from LangSmith
|
95
|
+
def send_trace(action, run_id, payload)
|
96
|
+
# Check if we should use the buffer
|
97
|
+
if defined?(LangsmithRunBuffer) && LangsmithrbRails::Config[:use_buffer]
|
98
|
+
# Store in buffer
|
99
|
+
buffer = LangsmithRunBuffer.new(
|
100
|
+
name: payload[:name],
|
101
|
+
run_type: payload[:run_type],
|
102
|
+
status: "pending",
|
103
|
+
request_id: payload.dig(:meta, :request_id),
|
104
|
+
user_ref: payload.dig(:meta, :user_ref),
|
105
|
+
run_id: run_id,
|
106
|
+
parent_run_id: payload[:parent_run_id],
|
107
|
+
started_at: payload[:started_at] || payload.dig(:meta, :started_at),
|
108
|
+
ended_at: payload[:ended_at],
|
109
|
+
meta: payload[:meta],
|
110
|
+
payload: payload,
|
111
|
+
error: payload[:error]
|
112
|
+
)
|
113
|
+
buffer.save
|
114
|
+
return { body: { "id" => buffer.id } } if action == "create"
|
115
|
+
return { status: 200 }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check if we should use a job
|
119
|
+
if defined?(LangsmithSendTraceJob)
|
120
|
+
if defined?(Sidekiq) && Sidekiq.server?
|
121
|
+
# Use Sidekiq directly
|
122
|
+
LangsmithSendTraceJob.perform_async(action, run_id, payload)
|
123
|
+
return { body: { "id" => SecureRandom.uuid } } if action == "create"
|
124
|
+
return { status: 200 }
|
125
|
+
elsif Rails.application.config.active_job.queue_adapter == :sidekiq
|
126
|
+
# Use ActiveJob with Sidekiq
|
127
|
+
LangsmithSendTraceJob.perform_later(action, run_id, payload)
|
128
|
+
return { body: { "id" => SecureRandom.uuid } } if action == "create"
|
129
|
+
return { status: 200 }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Fall back to direct API call
|
134
|
+
client = LangsmithrbRails::Client.new
|
135
|
+
|
136
|
+
if action == "create"
|
137
|
+
client.create_run(payload)
|
138
|
+
else
|
139
|
+
client.update_run(run_id, payload)
|
140
|
+
end
|
141
|
+
rescue => e
|
142
|
+
Rails.logger.error("LangSmith trace error: #{e.message}")
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Concern for adding LangSmith tracing to background jobs
|
4
|
+
module LangsmithTracedJob
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# Add around_perform callback to trace job execution
|
9
|
+
around_perform do |job, block|
|
10
|
+
# Skip tracing if sampling rate check fails
|
11
|
+
if rand > LangsmithrbRails::Config[:sampling_rate]
|
12
|
+
block.call
|
13
|
+
return
|
14
|
+
end
|
15
|
+
|
16
|
+
# Extract parent run ID from arguments or Current
|
17
|
+
parent_run_id = extract_parent_run_id(job.arguments)
|
18
|
+
|
19
|
+
# Create metadata for the trace
|
20
|
+
meta = {
|
21
|
+
job_class: job.class.name,
|
22
|
+
job_id: job.job_id,
|
23
|
+
queue_name: job.queue_name,
|
24
|
+
env: LangsmithrbRails::Config[:env]
|
25
|
+
}
|
26
|
+
|
27
|
+
# Add arguments if not sensitive
|
28
|
+
unless LangsmithrbRails::Config[:redact_job_arguments]
|
29
|
+
meta[:arguments] = LangsmithrbRails::Redactor.scrub(job.arguments)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a client
|
33
|
+
client = LangsmithrbRails::Client.new
|
34
|
+
|
35
|
+
# Get the start time
|
36
|
+
started = Time.now
|
37
|
+
|
38
|
+
# Create the run
|
39
|
+
run_payload = {
|
40
|
+
name: "#{job.class.name}#perform",
|
41
|
+
run_type: "job",
|
42
|
+
meta: meta.merge(started_at: started.iso8601),
|
43
|
+
parent_run_id: parent_run_id
|
44
|
+
}
|
45
|
+
|
46
|
+
create_response = send_trace("create", nil, run_payload)
|
47
|
+
run_id = create_response&.dig(:body, "id")
|
48
|
+
|
49
|
+
# Store the run ID in thread local for nested traces
|
50
|
+
Thread.current[:langsmith_run_id] = run_id if run_id
|
51
|
+
|
52
|
+
# Execute the job
|
53
|
+
result = block.call
|
54
|
+
|
55
|
+
# Update the run with completion
|
56
|
+
if run_id
|
57
|
+
send_trace("update", run_id, {
|
58
|
+
ended_at: Time.now.iso8601,
|
59
|
+
output: { status: "completed" }
|
60
|
+
})
|
61
|
+
end
|
62
|
+
|
63
|
+
# Clean up thread local
|
64
|
+
Thread.current[:langsmith_run_id] = nil
|
65
|
+
|
66
|
+
# Return the result
|
67
|
+
result
|
68
|
+
rescue => e
|
69
|
+
# Update the run with the error
|
70
|
+
if run_id
|
71
|
+
begin
|
72
|
+
send_trace("update", run_id, {
|
73
|
+
ended_at: Time.now.iso8601,
|
74
|
+
error: e.message
|
75
|
+
})
|
76
|
+
rescue => trace_error
|
77
|
+
Rails.logger.error("LangSmith error update failed: #{trace_error.message}")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clean up thread local
|
82
|
+
Thread.current[:langsmith_run_id] = nil
|
83
|
+
|
84
|
+
# Re-raise the original error
|
85
|
+
raise
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Extract parent run ID from job arguments or Current
|
92
|
+
# @param arguments [Array] Job arguments
|
93
|
+
# @return [String, nil] Parent run ID
|
94
|
+
def extract_parent_run_id(arguments)
|
95
|
+
# Check if any argument is a hash with a langsmith_run_id key
|
96
|
+
arguments.each do |arg|
|
97
|
+
if arg.is_a?(Hash) && arg.key?(:langsmith_run_id)
|
98
|
+
return arg[:langsmith_run_id]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if Current has a langsmith_run_id
|
103
|
+
if defined?(Current) && Current.respond_to?(:langsmith_run_id) && Current.langsmith_run_id
|
104
|
+
return Current.langsmith_run_id
|
105
|
+
end
|
106
|
+
|
107
|
+
# Fall back to thread local
|
108
|
+
Thread.current[:langsmith_run_id]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Send a trace to LangSmith
|
112
|
+
# @param action [String] Action to perform (create or update)
|
113
|
+
# @param run_id [String, nil] Run ID for updates
|
114
|
+
# @param payload [Hash] Payload to send
|
115
|
+
# @return [Hash, nil] Response from LangSmith
|
116
|
+
def send_trace(action, run_id, payload)
|
117
|
+
# Check if we should use the buffer
|
118
|
+
if defined?(LangsmithRunBuffer) && LangsmithrbRails::Config[:use_buffer]
|
119
|
+
# Store in buffer
|
120
|
+
buffer = LangsmithRunBuffer.new(
|
121
|
+
name: payload[:name],
|
122
|
+
run_type: payload[:run_type],
|
123
|
+
status: "pending",
|
124
|
+
request_id: payload.dig(:meta, :request_id),
|
125
|
+
user_ref: payload.dig(:meta, :user_ref),
|
126
|
+
run_id: run_id,
|
127
|
+
parent_run_id: payload[:parent_run_id],
|
128
|
+
started_at: payload[:started_at] || payload.dig(:meta, :started_at),
|
129
|
+
ended_at: payload[:ended_at],
|
130
|
+
meta: payload[:meta],
|
131
|
+
payload: payload,
|
132
|
+
error: payload[:error]
|
133
|
+
)
|
134
|
+
buffer.save
|
135
|
+
return { body: { "id" => buffer.id } } if action == "create"
|
136
|
+
return { status: 200 }
|
137
|
+
end
|
138
|
+
|
139
|
+
# Fall back to direct API call
|
140
|
+
client = LangsmithrbRails::Client.new
|
141
|
+
|
142
|
+
if action == "create"
|
143
|
+
client.create_run(payload)
|
144
|
+
else
|
145
|
+
client.update_run(run_id, payload)
|
146
|
+
end
|
147
|
+
rescue => e
|
148
|
+
Rails.logger.error("LangSmith trace error: #{e.message}")
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LangsmithrbRails
|
4
|
+
# Middleware for tracing Rails requests in LangSmith
|
5
|
+
class RequestTracing
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
# Skip tracing if sampling rate check fails
|
12
|
+
return @app.call(env) if rand > LangsmithrbRails::Config[:sampling_rate]
|
13
|
+
|
14
|
+
req = ActionDispatch::Request.new(env)
|
15
|
+
|
16
|
+
# Prepare metadata for the trace
|
17
|
+
meta = {
|
18
|
+
path: req.path,
|
19
|
+
method: req.request_method,
|
20
|
+
request_id: req.request_id,
|
21
|
+
env: LangsmithrbRails::Config[:env]
|
22
|
+
}
|
23
|
+
|
24
|
+
# Add user reference if available (and not in development/test)
|
25
|
+
if defined?(Current) && Current.respond_to?(:user) && Current.user && !%w[development test].include?(Rails.env)
|
26
|
+
# Use a hash of the user ID to avoid sending PII
|
27
|
+
meta[:user_ref] = Digest::SHA256.hexdigest(Current.user.id.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create the run in LangSmith
|
31
|
+
client = LangsmithrbRails::Client.new
|
32
|
+
started = Time.now
|
33
|
+
|
34
|
+
# Create the run asynchronously if possible
|
35
|
+
run = create_run_async(client, {
|
36
|
+
name: "rails_request",
|
37
|
+
run_type: "request",
|
38
|
+
meta: meta.merge(started_at: started.iso8601)
|
39
|
+
})
|
40
|
+
|
41
|
+
# Store run ID in thread local for child spans
|
42
|
+
Thread.current[:langsmith_run_id] = run&.dig(:body, "id")
|
43
|
+
|
44
|
+
# Process the request
|
45
|
+
status, headers, body = @app.call(env)
|
46
|
+
|
47
|
+
# Update the run asynchronously if possible
|
48
|
+
if run&.dig(:body, "id")
|
49
|
+
update_run_async(client, run.dig(:body, "id"), {
|
50
|
+
ended_at: Time.now.iso8601,
|
51
|
+
output: { status: status }
|
52
|
+
})
|
53
|
+
end
|
54
|
+
|
55
|
+
# Clean up thread local
|
56
|
+
Thread.current[:langsmith_run_id] = nil
|
57
|
+
|
58
|
+
[status, headers, body]
|
59
|
+
rescue => e
|
60
|
+
# Log error but don't fail the request
|
61
|
+
Rails.logger.error("LangSmith tracing error: #{e.message}")
|
62
|
+
|
63
|
+
# Clean up thread local
|
64
|
+
Thread.current[:langsmith_run_id] = nil
|
65
|
+
|
66
|
+
# Continue with the request
|
67
|
+
@app.call(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Create a run asynchronously
|
73
|
+
def create_run_async(client, payload)
|
74
|
+
if defined?(Sidekiq) && Sidekiq.server?
|
75
|
+
# Use Sidekiq if available
|
76
|
+
LangsmithSendTraceJob.perform_async("create", nil, payload)
|
77
|
+
nil
|
78
|
+
elsif Rails.application.config.active_job.queue_adapter == :sidekiq
|
79
|
+
# Use ActiveJob with Sidekiq
|
80
|
+
LangsmithSendTraceJob.perform_later("create", nil, payload)
|
81
|
+
nil
|
82
|
+
else
|
83
|
+
# Use a lightweight thread
|
84
|
+
Thread.new do
|
85
|
+
begin
|
86
|
+
client.create_run(payload)
|
87
|
+
rescue => e
|
88
|
+
Rails.logger.error("LangSmith async create error: #{e.message}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return a synchronous result for the run ID
|
93
|
+
client.create_run(payload)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Update a run asynchronously
|
98
|
+
def update_run_async(client, run_id, payload)
|
99
|
+
if defined?(Sidekiq) && Sidekiq.server?
|
100
|
+
# Use Sidekiq if available
|
101
|
+
LangsmithSendTraceJob.perform_async("update", run_id, payload)
|
102
|
+
elsif Rails.application.config.active_job.queue_adapter == :sidekiq
|
103
|
+
# Use ActiveJob with Sidekiq
|
104
|
+
LangsmithSendTraceJob.perform_later("update", run_id, payload)
|
105
|
+
else
|
106
|
+
# Use a lightweight thread
|
107
|
+
Thread.new do
|
108
|
+
begin
|
109
|
+
client.update_run(run_id, payload)
|
110
|
+
rescue => e
|
111
|
+
Rails.logger.error("LangSmith async update error: #{e.message}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|