langsmithrb_rails 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -0
  3. data/.rspec_status +161 -0
  4. data/CHANGELOG.md +38 -0
  5. data/Gemfile +20 -0
  6. data/Gemfile.lock +321 -0
  7. data/LICENSE +21 -0
  8. data/README.md +421 -0
  9. data/Rakefile +10 -0
  10. data/langsmithrb_rails-0.1.0.gem +0 -0
  11. data/langsmithrb_rails-0.1.1.gem +0 -0
  12. data/langsmithrb_rails.gemspec +45 -0
  13. data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
  14. data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
  15. data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
  16. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
  17. data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
  18. data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
  19. data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
  20. data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
  21. data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
  22. data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
  23. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
  24. data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
  25. data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
  26. data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
  27. data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
  28. data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
  29. data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
  30. data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
  31. data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
  32. data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
  33. data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
  34. data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
  35. data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
  36. data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
  37. data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
  38. data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
  39. data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
  40. data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
  41. data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
  42. data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
  43. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
  44. data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
  45. data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
  46. data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
  47. data/lib/langsmithrb_rails/client.rb +292 -0
  48. data/lib/langsmithrb_rails/config.rb +169 -0
  49. data/lib/langsmithrb_rails/evaluation/evaluator.rb +178 -0
  50. data/lib/langsmithrb_rails/evaluation/llm_evaluator.rb +154 -0
  51. data/lib/langsmithrb_rails/evaluation/string_evaluator.rb +158 -0
  52. data/lib/langsmithrb_rails/evaluation.rb +76 -0
  53. data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
  54. data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
  55. data/lib/langsmithrb_rails/langsmith.rb +35 -0
  56. data/lib/langsmithrb_rails/otel/exporter.rb +120 -0
  57. data/lib/langsmithrb_rails/otel.rb +135 -0
  58. data/lib/langsmithrb_rails/railtie.rb +33 -0
  59. data/lib/langsmithrb_rails/redactor.rb +76 -0
  60. data/lib/langsmithrb_rails/run_trees.rb +157 -0
  61. data/lib/langsmithrb_rails/version.rb +5 -0
  62. data/lib/langsmithrb_rails/wrappers/anthropic.rb +146 -0
  63. data/lib/langsmithrb_rails/wrappers/base.rb +81 -0
  64. data/lib/langsmithrb_rails/wrappers/llm.rb +151 -0
  65. data/lib/langsmithrb_rails/wrappers/openai.rb +193 -0
  66. data/lib/langsmithrb_rails/wrappers.rb +41 -0
  67. data/lib/langsmithrb_rails.rb +151 -0
  68. data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
  69. metadata +74 -7
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module OTEL
5
+ # OpenTelemetry exporter for LangSmith
6
+ class Exporter
7
+ # Initialize a new OpenTelemetry exporter
8
+ # @param api_key [String] LangSmith API key
9
+ # @param api_url [String] LangSmith API URL
10
+ def initialize(api_key: nil, api_url: nil)
11
+ @api_key = api_key || Config[:api_key]
12
+ @api_url = api_url || Config[:api_url]
13
+ @client = LangsmithrbRails::Client.new(api_key: @api_key, api_url: @api_url)
14
+ end
15
+
16
+ # Export spans to LangSmith
17
+ # @param spans [Array<OpenTelemetry::SDK::Trace::SpanData>] Spans to export
18
+ # @return [Integer] Export result (success, failure, etc.)
19
+ def export(spans)
20
+ return :success if spans.empty?
21
+
22
+ spans.each do |span|
23
+ export_span(span)
24
+ end
25
+
26
+ :success
27
+ rescue => e
28
+ LangsmithrbRails.logger.error("Failed to export spans: #{e.message}")
29
+ :failure
30
+ end
31
+
32
+ # Shutdown the exporter
33
+ # @param timeout [Integer] Timeout in seconds
34
+ # @return [Boolean] True if shutdown was successful
35
+ def shutdown(timeout = 0)
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ # Export a single span to LangSmith
42
+ # @param span [OpenTelemetry::SDK::Trace::SpanData] Span to export
43
+ def export_span(span)
44
+ # Convert span to LangSmith run
45
+ run_data = convert_span_to_run(span)
46
+
47
+ # Create or update the run in LangSmith
48
+ if span.parent_span_id.nil?
49
+ @client.create_run(run_data)
50
+ else
51
+ @client.update_run(run_data[:id], run_data)
52
+ end
53
+ end
54
+
55
+ # Convert an OpenTelemetry span to a LangSmith run
56
+ # @param span [OpenTelemetry::SDK::Trace::SpanData] Span to convert
57
+ # @return [Hash] LangSmith run data
58
+ def convert_span_to_run(span)
59
+ # Extract span attributes
60
+ attributes = span.attributes.to_h
61
+
62
+ # Basic run data
63
+ run_data = {
64
+ id: span.span_id.to_s,
65
+ name: span.name,
66
+ start_time: span.start_timestamp.to_time.utc.iso8601,
67
+ end_time: span.end_timestamp.to_time.utc.iso8601,
68
+ status: span.status.code == 0 ? "success" : "error"
69
+ }
70
+
71
+ # Add parent run ID if present
72
+ if span.parent_span_id
73
+ run_data[:parent_run_id] = span.parent_span_id.to_s
74
+ end
75
+
76
+ # Add error if present
77
+ if span.status.code != 0
78
+ run_data[:error] = span.status.description || "Unknown error"
79
+ end
80
+
81
+ # Add inputs and outputs from attributes
82
+ if attributes[:inputs]
83
+ run_data[:inputs] = parse_json_attribute(attributes[:inputs])
84
+ end
85
+
86
+ if attributes[:outputs]
87
+ run_data[:outputs] = parse_json_attribute(attributes[:outputs])
88
+ end
89
+
90
+ # Add run type
91
+ run_data[:run_type] = attributes[:run_type] || "chain"
92
+
93
+ # Add project name if present
94
+ if attributes[:project_name]
95
+ run_data[:session_name] = attributes[:project_name]
96
+ end
97
+
98
+ # Add tags if present
99
+ if attributes[:tags]
100
+ run_data[:tags] = parse_json_attribute(attributes[:tags])
101
+ end
102
+
103
+ run_data
104
+ end
105
+
106
+ # Parse a JSON attribute
107
+ # @param value [String] JSON string
108
+ # @return [Hash, Array, String] Parsed JSON or original string
109
+ def parse_json_attribute(value)
110
+ return value unless value.is_a?(String)
111
+
112
+ begin
113
+ JSON.parse(value)
114
+ rescue JSON::ParserError
115
+ value
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "otel/exporter"
4
+
5
+ module LangsmithrbRails
6
+ # OpenTelemetry integration for LangSmith
7
+ module OTEL
8
+ class << self
9
+ # Initialize OpenTelemetry integration
10
+ # @param api_key [String] LangSmith API key
11
+ # @param api_url [String] LangSmith API URL
12
+ # @param service_name [String] Service name for OpenTelemetry
13
+ # @param service_version [String] Service version for OpenTelemetry
14
+ # @return [Boolean] True if initialization was successful
15
+ def init(api_key: nil, api_url: nil, service_name: "langsmithrb-rails", service_version: LangsmithrbRails::VERSION)
16
+ return false unless otel_available?
17
+
18
+ require "opentelemetry/sdk"
19
+ require "opentelemetry/exporter/otlp"
20
+ require "opentelemetry/instrumentation/all"
21
+
22
+ # Create LangSmith exporter
23
+ langsmith_exporter = Exporter.new(api_key: api_key, api_url: api_url)
24
+
25
+ # Configure OpenTelemetry
26
+ OpenTelemetry::SDK.configure do |c|
27
+ c.service_name = service_name
28
+ c.service_version = service_version
29
+
30
+ # Add LangSmith exporter
31
+ c.add_span_processor(
32
+ OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(langsmith_exporter)
33
+ )
34
+
35
+ # Use batch processor for better performance
36
+ c.add_span_processor(
37
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
38
+ langsmith_exporter,
39
+ max_queue_size: 1000,
40
+ max_export_batch_size: 100,
41
+ schedule_delay_millis: 5000
42
+ )
43
+ )
44
+
45
+ # Install all available instrumentation
46
+ c.use_all()
47
+ end
48
+
49
+ true
50
+ rescue => e
51
+ LangsmithrbRails.logger.error("Failed to initialize OpenTelemetry: #{e.message}")
52
+ false
53
+ end
54
+
55
+ # Create a traced span
56
+ # @param name [String] Span name
57
+ # @param attributes [Hash] Span attributes
58
+ # @param kind [Symbol] Span kind
59
+ # @yield Block to execute within the span
60
+ # @return [Object] Result of the block
61
+ def trace(name, attributes: {}, kind: :internal, &block)
62
+ return yield unless otel_available? && otel_enabled?
63
+
64
+ tracer = OpenTelemetry.tracer_provider.tracer("langsmithrb_rails", LangsmithrbRails::VERSION)
65
+
66
+ tracer.in_span(name, attributes: attributes, kind: kind, &block)
67
+ end
68
+
69
+ # Create a traced span for an LLM operation
70
+ # @param name [String] Operation name
71
+ # @param inputs [Hash] Input data
72
+ # @param run_type [String] Type of run (e.g., "llm", "chain")
73
+ # @param project_name [String] Optional project name
74
+ # @param tags [Array<String>] Optional tags
75
+ # @yield Block to execute within the span
76
+ # @return [Object] Result of the block
77
+ def trace_llm(name, inputs:, run_type: "llm", project_name: nil, tags: [], &block)
78
+ return yield unless otel_available? && otel_enabled?
79
+
80
+ attributes = {
81
+ inputs: inputs.to_json,
82
+ run_type: run_type
83
+ }
84
+
85
+ if project_name
86
+ attributes[:project_name] = project_name
87
+ end
88
+
89
+ if tags.any?
90
+ attributes[:tags] = tags.to_json
91
+ end
92
+
93
+ trace(name, attributes: attributes) do |span|
94
+ begin
95
+ result = yield
96
+
97
+ # Add outputs to span
98
+ if result
99
+ span.add_attributes(outputs: { result: result }.to_json)
100
+ end
101
+
102
+ result
103
+ rescue => e
104
+ # Add error to span
105
+ if span.respond_to?(:record_exception)
106
+ span.record_exception(e)
107
+ if defined?(OpenTelemetry::Trace::Status)
108
+ span.status = OpenTelemetry::Trace::Status.error(e.message)
109
+ end
110
+ end
111
+ raise e
112
+ end
113
+ end
114
+ end
115
+
116
+ # Check if OpenTelemetry is available
117
+ # @return [Boolean] True if OpenTelemetry is available
118
+ def otel_available?
119
+ @otel_available ||= begin
120
+ require "opentelemetry"
121
+ require "opentelemetry/sdk"
122
+ true
123
+ rescue LoadError
124
+ false
125
+ end
126
+ end
127
+
128
+ # Check if OpenTelemetry is enabled
129
+ # @return [Boolean] True if OpenTelemetry is enabled
130
+ def otel_enabled?
131
+ Config[:otel_enabled]
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "langsmithrb"
4
+
5
+ module LangsmithrbRails
6
+ # Rails integration for LangsmithrbRails
7
+ class Railtie < Rails::Railtie
8
+ initializer "langsmithrb_rails" do
9
+ # Configure LangSmith if enabled
10
+ if LangsmithrbRails.config.enabled && LangsmithrbRails.config.api_key
11
+ # Configure the langsmithrb gem
12
+ Langsmithrb.configure do |config|
13
+ config.api_key = LangsmithrbRails.config.api_key
14
+ config.project_name = LangsmithrbRails.config.project_name
15
+ config.tracing_enabled = true
16
+ end
17
+
18
+ # Also configure our wrapper for consistency
19
+ LangsmithrbRails::LangSmith.configure(
20
+ api_key: LangsmithrbRails.config.api_key,
21
+ project_name: LangsmithrbRails.config.project_name,
22
+ tracing: true
23
+ )
24
+
25
+ Rails.logger.info "LangSmith tracing enabled"
26
+ end
27
+ end
28
+
29
+ generators do
30
+ require_relative "generators/langsmithrb_rails/langsmith_generator"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ # PII redaction utility for LangSmith traces
5
+ class Redactor
6
+ # Common PII patterns
7
+ PATTERNS = {
8
+ email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
9
+ phone: /\b(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b/,
10
+ credit_card: /\b(?:\d{4}[-\s]?){3}\d{4}\b|\b\d{13,16}\b/,
11
+ ssn: /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/,
12
+ ip_address: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
13
+ }.freeze
14
+
15
+ class << self
16
+ # Allowlist of patterns that should not be redacted
17
+ attr_accessor :allowlist
18
+
19
+ # Custom patterns to redact beyond the defaults
20
+ attr_accessor :custom_patterns
21
+
22
+ # Initialize the class variables
23
+ def setup
24
+ @allowlist = []
25
+ @custom_patterns = {}
26
+ end
27
+
28
+ # Scrub PII from the input
29
+ # @param input [Object] Input to scrub (String, Hash, Array, etc.)
30
+ # @return [Object] Redacted copy of the input
31
+ def scrub(input)
32
+ return input unless Config[:redact_by_default]
33
+
34
+ case input
35
+ when String
36
+ scrub_string(input)
37
+ when Hash
38
+ input.transform_values { |v| scrub(v) }
39
+ when Array
40
+ input.map { |item| scrub(item) }
41
+ else
42
+ input
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # Scrub PII from a string
49
+ # @param text [String] String to scrub
50
+ # @return [String] Redacted string
51
+ def scrub_string(text)
52
+ return text unless text.is_a?(String)
53
+
54
+ result = text.dup
55
+
56
+ # Skip redaction for allowlisted patterns
57
+ return result if @allowlist&.any? { |pattern| text.match?(pattern) }
58
+
59
+ # Apply default patterns
60
+ PATTERNS.each do |type, pattern|
61
+ result.gsub!(pattern) { |match| "[REDACTED #{type.upcase}]" }
62
+ end
63
+
64
+ # Apply custom patterns
65
+ @custom_patterns&.each do |type, pattern|
66
+ result.gsub!(pattern) { |match| "[REDACTED #{type.upcase}]" }
67
+ end
68
+
69
+ result
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Initialize class variables
76
+ LangsmithrbRails::Redactor.setup
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module LangsmithrbRails
6
+ # Run trees for hierarchical tracing
7
+ class RunTree
8
+ attr_reader :run_id, :name, :run_type, :inputs, :parent_run_id, :start_time, :children, :project_name, :tags
9
+
10
+ # Initialize a new run tree
11
+ # @param name [String] Name of the run
12
+ # @param run_type [String] Type of run (e.g., "llm", "chain")
13
+ # @param inputs [Hash] Input data
14
+ # @param run_id [String] Optional run ID
15
+ # @param parent_run_id [String] Optional parent run ID
16
+ # @param project_name [String] Optional project name
17
+ # @param tags [Array<String>] Optional tags
18
+ def initialize(name:, run_type:, inputs:, run_id: nil, parent_run_id: nil, project_name: nil, tags: [])
19
+ @run_id = run_id || SecureRandom.uuid
20
+ @name = name
21
+ @run_type = run_type
22
+ @inputs = inputs
23
+ @parent_run_id = parent_run_id
24
+ @start_time = Time.now.utc
25
+ @children = []
26
+ @project_name = project_name
27
+ @tags = tags
28
+ @client = LangsmithrbRails::Client.new
29
+ @ended = false
30
+
31
+ # Create the run in LangSmith
32
+ create_run
33
+ end
34
+
35
+ # Create a child run
36
+ # @param name [String] Name of the child run
37
+ # @param run_type [String] Type of run (e.g., "llm", "chain")
38
+ # @param inputs [Hash] Input data
39
+ # @param tags [Array<String>] Optional tags
40
+ # @return [RunTree] The child run tree
41
+ def create_child(name:, run_type:, inputs:, tags: [])
42
+ child_run_id = SecureRandom.uuid
43
+
44
+ child_run = RunTree.new(
45
+ run_id: child_run_id,
46
+ parent_run_id: @run_id,
47
+ name: name,
48
+ run_type: run_type,
49
+ inputs: inputs,
50
+ project_name: @project_name,
51
+ tags: tags
52
+ )
53
+
54
+ @children << child_run
55
+ child_run
56
+ end
57
+
58
+ # End the run with outputs
59
+ # @param outputs [Hash] Output data
60
+ # @param error [String] Optional error message
61
+ def end(outputs: {}, error: nil)
62
+ return if @ended
63
+
64
+ # End all children first
65
+ @children.each { |child| child.end unless child.ended? }
66
+
67
+ # Update the run in LangSmith
68
+ update_run(outputs, error)
69
+ @ended = true
70
+ end
71
+
72
+ # Check if the run has ended
73
+ # @return [Boolean] True if the run has ended
74
+ def ended?
75
+ @ended
76
+ end
77
+
78
+ private
79
+
80
+ # Create the run in LangSmith
81
+ def create_run
82
+ run_data = {
83
+ id: @run_id,
84
+ name: @name,
85
+ run_type: @run_type,
86
+ inputs: @inputs,
87
+ start_time: @start_time.iso8601,
88
+ parent_run_id: @parent_run_id,
89
+ execution_order: 1,
90
+ serialized: { name: @name },
91
+ session_name: @project_name,
92
+ tags: @tags
93
+ }.compact
94
+
95
+ response = @client.create_run(run_data)
96
+
97
+ unless response[:status] >= 200 && response[:status] < 300
98
+ LangsmithrbRails.logger.error("Failed to create run: #{response[:error] || response[:body]}")
99
+ end
100
+ end
101
+
102
+ # Update the run in LangSmith
103
+ # @param outputs [Hash] Output data
104
+ # @param error [String] Optional error message
105
+ def update_run(outputs, error)
106
+ run_data = {
107
+ outputs: outputs,
108
+ end_time: Time.now.utc.iso8601
109
+ }
110
+
111
+ if error
112
+ run_data[:error] = error
113
+ run_data[:status] = "error"
114
+ else
115
+ run_data[:status] = "success"
116
+ end
117
+
118
+ response = @client.update_run(@run_id, run_data)
119
+
120
+ unless response[:status] >= 200 && response[:status] < 300
121
+ LangsmithrbRails.logger.error("Failed to update run: #{response[:error] || response[:body]}")
122
+ end
123
+ end
124
+ end
125
+
126
+ # Context manager for run trees
127
+ class RunContext
128
+ # Start a new run context
129
+ # @param name [String] Name of the run
130
+ # @param run_type [String] Type of run (e.g., "llm", "chain")
131
+ # @param inputs [Hash] Input data
132
+ # @param parent_run_id [String] Optional parent run ID
133
+ # @param project_name [String] Optional project name
134
+ # @param tags [Array<String>] Optional tags
135
+ # @yield [RunTree] The run tree
136
+ # @return [Object] The result of the block
137
+ def self.run(name:, run_type:, inputs:, parent_run_id: nil, project_name: nil, tags: [], &block)
138
+ run_tree = RunTree.new(
139
+ name: name,
140
+ run_type: run_type,
141
+ inputs: inputs,
142
+ parent_run_id: parent_run_id,
143
+ project_name: project_name,
144
+ tags: tags
145
+ )
146
+
147
+ begin
148
+ result = yield(run_tree)
149
+ run_tree.end(outputs: { result: result })
150
+ result
151
+ rescue => e
152
+ run_tree.end(error: e.message)
153
+ raise e
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ VERSION = "0.3.0"
5
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module LangsmithrbRails
6
+ module Wrappers
7
+ # Wrapper for Anthropic client
8
+ module Anthropic
9
+ # Wrap an Anthropic client with LangSmith tracing
10
+ # @param client [Object] The Anthropic client to wrap
11
+ # @param project_name [String] Optional project name for traces
12
+ # @param tags [Array<String>] Optional tags for traces
13
+ # @return [Object] The wrapped client
14
+ def self.wrap(client, project_name: nil, tags: [])
15
+ # Create a wrapper class that inherits from the client's class
16
+ wrapper_class = Class.new(client.class) do
17
+ attr_reader :original_client, :project_name, :tags
18
+
19
+ def initialize(original_client, project_name, tags)
20
+ @original_client = original_client
21
+ @project_name = project_name
22
+ @tags = tags
23
+ end
24
+
25
+ # Wrap messages (Claude API)
26
+ def messages(messages:, model: nil, max_tokens: nil, temperature: nil, **params)
27
+ # Prepare inputs for tracing
28
+ inputs = {
29
+ messages: messages,
30
+ model: model,
31
+ max_tokens: max_tokens,
32
+ temperature: temperature
33
+ }.merge(params).compact
34
+
35
+ # Create a run
36
+ run_id = nil
37
+ begin
38
+ run = LangsmithrbRails::Wrappers::Base.create_run(
39
+ "anthropic.messages",
40
+ inputs,
41
+ run_type: "llm",
42
+ project_name: project_name,
43
+ tags: tags
44
+ )
45
+ run_id = run&.dig("id")
46
+ rescue => e
47
+ LangsmithrbRails.logger.error("Failed to create LangSmith run: #{e.message}")
48
+ end
49
+
50
+ # Call the original method
51
+ begin
52
+ result = original_client.messages(
53
+ messages: messages,
54
+ model: model,
55
+ max_tokens: max_tokens,
56
+ temperature: temperature,
57
+ **params
58
+ )
59
+
60
+ # Update the run with the result
61
+ if run_id
62
+ outputs = { response: result }
63
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, outputs)
64
+ end
65
+
66
+ result
67
+ rescue => e
68
+ # Update the run with the error
69
+ if run_id
70
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, {}, error: e.message)
71
+ end
72
+ raise e
73
+ end
74
+ end
75
+
76
+ # Wrap completions (older Claude API)
77
+ def completions(prompt:, model: nil, max_tokens_to_sample: nil, temperature: nil, **params)
78
+ # Prepare inputs for tracing
79
+ inputs = {
80
+ prompt: prompt,
81
+ model: model,
82
+ max_tokens_to_sample: max_tokens_to_sample,
83
+ temperature: temperature
84
+ }.merge(params).compact
85
+
86
+ # Create a run
87
+ run_id = nil
88
+ begin
89
+ run = LangsmithrbRails::Wrappers::Base.create_run(
90
+ "anthropic.completions",
91
+ inputs,
92
+ run_type: "llm",
93
+ project_name: project_name,
94
+ tags: tags
95
+ )
96
+ run_id = run&.dig("id")
97
+ rescue => e
98
+ LangsmithrbRails.logger.error("Failed to create LangSmith run: #{e.message}")
99
+ end
100
+
101
+ # Call the original method
102
+ begin
103
+ result = original_client.completions(
104
+ prompt: prompt,
105
+ model: model,
106
+ max_tokens_to_sample: max_tokens_to_sample,
107
+ temperature: temperature,
108
+ **params
109
+ )
110
+
111
+ # Update the run with the result
112
+ if run_id
113
+ outputs = { response: result }
114
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, outputs)
115
+ end
116
+
117
+ result
118
+ rescue => e
119
+ # Update the run with the error
120
+ if run_id
121
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, {}, error: e.message)
122
+ end
123
+ raise e
124
+ end
125
+ end
126
+
127
+ # Forward other methods to the original client
128
+ def method_missing(method_name, *args, **kwargs, &block)
129
+ if original_client.respond_to?(method_name)
130
+ original_client.send(method_name, *args, **kwargs, &block)
131
+ else
132
+ super
133
+ end
134
+ end
135
+
136
+ def respond_to_missing?(method_name, include_private = false)
137
+ original_client.respond_to?(method_name, include_private) || super
138
+ end
139
+ end
140
+
141
+ # Create and return an instance of the wrapper class
142
+ wrapper_class.new(client, project_name, tags)
143
+ end
144
+ end
145
+ end
146
+ end