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,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