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.
- checksums.yaml +4 -4
- data/.rspec_status +158 -79
- data/CHANGELOG.md +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +153 -0
- data/langsmithrb_rails-0.1.1.gem +0 -0
- data/lib/langsmithrb_rails/client.rb +217 -2
- data/lib/langsmithrb_rails/config.rb +143 -46
- data/lib/langsmithrb_rails/evaluation/evaluator.rb +178 -0
- data/lib/langsmithrb_rails/evaluation/llm_evaluator.rb +154 -0
- data/lib/langsmithrb_rails/evaluation/string_evaluator.rb +158 -0
- data/lib/langsmithrb_rails/evaluation.rb +76 -0
- data/lib/langsmithrb_rails/otel/exporter.rb +120 -0
- data/lib/langsmithrb_rails/otel.rb +135 -0
- data/lib/langsmithrb_rails/run_trees.rb +157 -0
- data/lib/langsmithrb_rails/version.rb +1 -1
- data/lib/langsmithrb_rails/wrappers/anthropic.rb +146 -0
- data/lib/langsmithrb_rails/wrappers/base.rb +81 -0
- data/lib/langsmithrb_rails/wrappers/llm.rb +151 -0
- data/lib/langsmithrb_rails/wrappers/openai.rb +193 -0
- data/lib/langsmithrb_rails/wrappers.rb +41 -0
- data/lib/langsmithrb_rails.rb +121 -1
- data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
- metadata +16 -2
@@ -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,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
|