langsmithrb_rails 0.1.1 → 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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LangsmithrbRails
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LangsmithrbRails
4
+ module Wrappers
5
+ # Base module for LLM provider wrappers
6
+ module Base
7
+ # Wrap a provider client with LangSmith tracing
8
+ # @param client [Object] The provider client to wrap
9
+ # @param project_name [String] Optional project name for traces
10
+ # @param tags [Array<String>] Optional tags for traces
11
+ # @return [Object] The wrapped client
12
+ def self.wrap(client, project_name: nil, tags: [])
13
+ # To be implemented by specific provider wrappers
14
+ raise NotImplementedError, "This method should be implemented by provider-specific wrappers"
15
+ end
16
+
17
+ # Create a run for a provider operation
18
+ # @param name [String] Name of the operation
19
+ # @param inputs [Hash] Input data
20
+ # @param run_type [String] Type of run (e.g., "llm", "chain")
21
+ # @param project_name [String] Optional project name
22
+ # @param tags [Array<String>] Optional tags
23
+ # @return [Hash] The created run data
24
+ def self.create_run(name, inputs, run_type: "llm", project_name: nil, tags: [])
25
+ client = LangsmithrbRails::Client.new
26
+
27
+ run_data = {
28
+ name: name,
29
+ inputs: inputs,
30
+ run_type: run_type,
31
+ start_time: Time.now.utc.iso8601,
32
+ execution_order: 1,
33
+ serialized: { name: name },
34
+ session_name: project_name,
35
+ tags: tags
36
+ }
37
+
38
+ response = client.create_run(run_data)
39
+
40
+ if response[:status] >= 200 && response[:status] < 300
41
+ response[:body]
42
+ else
43
+ LangsmithrbRails.logger.error("Failed to create run: #{response[:error] || response[:body]}")
44
+ nil
45
+ end
46
+ end
47
+
48
+ # Update a run with outputs and end time
49
+ # @param run_id [String] ID of the run to update
50
+ # @param outputs [Hash] Output data
51
+ # @param error [String] Optional error message
52
+ # @return [Hash] The updated run data
53
+ def self.update_run(run_id, outputs, error: nil)
54
+ return unless run_id
55
+
56
+ client = LangsmithrbRails::Client.new
57
+
58
+ run_data = {
59
+ outputs: outputs,
60
+ end_time: Time.now.utc.iso8601
61
+ }
62
+
63
+ if error
64
+ run_data[:error] = error
65
+ run_data[:status] = "error"
66
+ else
67
+ run_data[:status] = "success"
68
+ end
69
+
70
+ response = client.update_run(run_id, run_data)
71
+
72
+ if response[:status] >= 200 && response[:status] < 300
73
+ response[:body]
74
+ else
75
+ LangsmithrbRails.logger.error("Failed to update run: #{response[:error] || response[:body]}")
76
+ nil
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module LangsmithrbRails
6
+ module Wrappers
7
+ # Wrapper for generic LLM providers
8
+ module LLM
9
+ # Wrap an LLM with LangSmith tracing
10
+ # @param llm [Object] The LLM 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 LLM
14
+ def self.wrap(llm, project_name: nil, tags: [])
15
+ # Create a wrapper class
16
+ wrapper_class = Class.new do
17
+ attr_reader :llm, :project_name, :tags
18
+
19
+ def initialize(llm, project_name, tags)
20
+ @llm = llm
21
+ @project_name = project_name
22
+ @tags = tags
23
+ end
24
+
25
+ # Wrap the call method
26
+ def call(prompt, **params)
27
+ # Prepare inputs for tracing
28
+ inputs = {
29
+ prompt: prompt
30
+ }.merge(params).compact
31
+
32
+ # Create a run
33
+ run_id = nil
34
+ begin
35
+ run = LangsmithrbRails::Wrappers::Base.create_run(
36
+ "llm.call",
37
+ inputs,
38
+ run_type: "llm",
39
+ project_name: project_name,
40
+ tags: tags
41
+ )
42
+ run_id = run&.dig("id")
43
+ rescue => e
44
+ LangsmithrbRails.logger.error("Failed to create LangSmith run: #{e.message}")
45
+ end
46
+
47
+ # Call the original method
48
+ begin
49
+ result = if llm.respond_to?(:call)
50
+ llm.call(prompt, **params)
51
+ elsif llm.is_a?(Proc)
52
+ llm.call(prompt, **params)
53
+ else
54
+ raise ArgumentError, "LLM must respond to 'call' or be a Proc"
55
+ end
56
+
57
+ # Update the run with the result
58
+ if run_id
59
+ outputs = { response: result }
60
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, outputs)
61
+ end
62
+
63
+ result
64
+ rescue => e
65
+ # Update the run with the error
66
+ if run_id
67
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, {}, error: e.message)
68
+ end
69
+ raise e
70
+ end
71
+ end
72
+
73
+ # Forward other methods to the original LLM
74
+ def method_missing(method_name, *args, **kwargs, &block)
75
+ if llm.respond_to?(method_name)
76
+ llm.send(method_name, *args, **kwargs, &block)
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ def respond_to_missing?(method_name, include_private = false)
83
+ llm.respond_to?(method_name, include_private) || super
84
+ end
85
+ end
86
+
87
+ # Create and return an instance of the wrapper class
88
+ wrapper_class.new(llm, project_name, tags)
89
+ end
90
+
91
+ # Create a traceable decorator for any method
92
+ # @param object [Object] The object with the method to trace
93
+ # @param method_name [Symbol] The name of the method to trace
94
+ # @param run_name [String] Name for the run
95
+ # @param project_name [String] Optional project name for traces
96
+ # @param tags [Array<String>] Optional tags for traces
97
+ # @return [Object] The object with the traced method
98
+ def self.traceable(object, method_name, run_name: nil, project_name: nil, tags: [])
99
+ run_name ||= "#{object.class.name}.#{method_name}"
100
+
101
+ # Store the original method
102
+ original_method = object.method(method_name)
103
+
104
+ # Define a new method that wraps the original
105
+ object.define_singleton_method(method_name) do |*args, **kwargs, &block|
106
+ # Prepare inputs for tracing
107
+ inputs = {
108
+ args: args,
109
+ kwargs: kwargs
110
+ }.compact
111
+
112
+ # Create a run
113
+ run_id = nil
114
+ begin
115
+ run = LangsmithrbRails::Wrappers::Base.create_run(
116
+ run_name,
117
+ inputs,
118
+ run_type: "chain",
119
+ project_name: project_name,
120
+ tags: tags
121
+ )
122
+ run_id = run&.dig("id")
123
+ rescue => e
124
+ LangsmithrbRails.logger.error("Failed to create LangSmith run: #{e.message}")
125
+ end
126
+
127
+ # Call the original method
128
+ begin
129
+ result = original_method.call(*args, **kwargs, &block)
130
+
131
+ # Update the run with the result
132
+ if run_id
133
+ outputs = { result: result }
134
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, outputs)
135
+ end
136
+
137
+ result
138
+ rescue => e
139
+ # Update the run with the error
140
+ if run_id
141
+ LangsmithrbRails::Wrappers::Base.update_run(run_id, {}, error: e.message)
142
+ end
143
+ raise e
144
+ end
145
+ end
146
+
147
+ object
148
+ end
149
+ end
150
+ end
151
+ end