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.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rspec_status +161 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +321 -0
- data/LICENSE +21 -0
- data/README.md +421 -0
- data/Rakefile +10 -0
- data/langsmithrb_rails-0.1.0.gem +0 -0
- data/langsmithrb_rails-0.1.1.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 +292 -0
- data/lib/langsmithrb_rails/config.rb +169 -0
- 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/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/otel/exporter.rb +120 -0
- data/lib/langsmithrb_rails/otel.rb +135 -0
- data/lib/langsmithrb_rails/railtie.rb +33 -0
- data/lib/langsmithrb_rails/redactor.rb +76 -0
- data/lib/langsmithrb_rails/run_trees.rb +157 -0
- data/lib/langsmithrb_rails/version.rb +5 -0
- 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 +151 -0
- data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
- 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,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
|